I am making a todo list program in Ruby. I have declared two classes List and Task. List is initialized with an empty array whilst Task has a name and a status (status is initialized as "incomplete") on creating a new Task. I want to edit the createtask method so I can check the task array of the List to see if a task already exists and if it does I don't want to create a new task. How would I do this?
class Task
attr_accessor :name, :status
def initialize(name, status="incomplete")
#name = name
#status = status
end
def to_s
"#{name.capitalize}: #{status.capitalize}"
end
end
class List
attr_accessor :tasksarray
def initialize
#tasksarray = []
end
def create_task(name)
new_task = Task.new(name)
tasksarray.push(new_task)
puts "New task #{new_task.name} has been added with status #{new_task.status}"
end
A method that can help you out here is detect (it's also called find, same method, different name) in the Enumerable mixin.
Use it to pass each element of your list to a block, it will find the first element for which the block returns true.
If no element is found, it returns nil.
In your case, you want to check whether an element has the same name as the one you're giving:
existing_task = #tasksarray.detect { |task| task.name == name }
if existing_task
puts "Not adding new task, found existing task with status #{ existing_task.status }"
else
new_task = Task.new(name)
#tasksarray.push(new_task)
puts "New task #{new_task.name} has been added with status #{new_task.status}"
end
You could to the same with Enumerable#any?
The comment you left on your question suggests that you only want a single list of tasks. You can do that by creating a class List, but you won't be creating instances of that class. Instead, the class itself will hold the list of tasks in a class instance variable. Consider the following.
Code
class Task
attr_accessor :name, :status
def initialize(name, status)
#name = name
#status = status
end
end
class List
singleton_class.send(:attr_reader, :tasks)
#tasks = []
def self.create_task(name, status="incomplete")
return nil if tasks.any? { |task| task.name == name }
new_task = Task.new(name, status)
tasks << new_task
new_task
end
end
Notice that I've removed the puts statements. That makes the methods more robust. We can provide an explanation in a separate method:
def explain(name, task)
if task
puts "New task '#{name}' with status '#{task.status}' has been added"
else
puts "'#{name}' is already in the list of tasks"
end
end
Examples
name = "Take out the papers"
task = List.create_task(name, "will do soon")
#=> #<Task:0x007ffa2a161058 #name="Take out the papers", #status="will do soon">
explain(name, task)
# New task 'Take out the papers' with status 'will do soon' has been added
name = "and the trash"
task = List.create_task(name)
#=> #<Task:0x007ffa2a149f20 #name="and the trash", #status="incomplete">
explain(name, task)
# New task 'and the trash' with status 'incomplete' has been added
name = "Take out the papers"
task = List.create_task(name)
#=> nil
explain(name, task)
# 'Take out the papers' is already in the list of tasks
List.tasks
#=> [#<Task:0x007ffa2a161058 #name="Take out the papers", #status="will do soon">,
# #<Task:0x007ffa2a149f20 #name="and the trash", #status="incomplete">]
List.tasks[1].name
#=> "and the trash"
Discussion
Note that, since we will not be creating instances of List, that class has no initialize method. The line:
singleton_class.send(:attr_reader, :tasks)
sends the method Module#attr_reader with argument :tasks to List's singleton class, thereby creating a getter for #tasks. Three other ways this is commonly done are:
singleton_class.class_eval{attr_reader :tasks}
and
class << self
attr_reader :tasks
end
and
module A
attr_reader :tasks
end
class Klass
extend A
end
Improving what we have
Since we are neither creating instances of List nor subclassing that class, we could make List a module, which would draw attention to the fact that we are not making use of the properties that distinguish a class from a module.
Rather than having instance variables #name and #status, it might makes sense to have a single instance variable that is a hash, that we might call #task. Among other things, that would make it easier if later we wished to add additional task attributes.
To determine if a task is in the list of tasks, we need to step through an array. If there are many tasks, that would be relatively inefficient. We could speed that up considerably by making the list a hash, whose keys are names of tasks and whose values are the task instances. That way, we only have to check to see if the hash has a key name, which is very fast.1.
In sum, let's do the following:
Replace the instance variables #name and #status in Tasks with a hash.
Replace the instance variable #task with a hash.
Make List a module rather than a class.
Revised code
class Task
attr_accessor :task
def initialize(name, status)
#task = { name: name, status: status }
end
end
module List
singleton_class.send(:attr_reader, :tasks)
#tasks = {}
def self.create_task(name, status = 'incomplete')
return nil if tasks.key?(name)
task = Task.new(name, status)
#tasks[name] = task
task
end
end
def explain(name, task)
if task
puts "New task '#{name}' with status '#{task.task[:status]}' has been added"
else
puts "'#{name}' is already in the list of tasks"
end
end
Examples
name = "Take out the papers"
task = List.create_task(name, "real soon now")
#=> #<Task:0x007f802305cf30 #task={:name=>"Take out the papers",
# :status=>"real soon now"}>
explain(name, task)
# New task 'Take out the papers' with status 'real soon now' has been added
name = "and the trash"
task = List.create_task(name)
#=> #<Task:0x007f8023045088 #task={:name=>"and the trash",
# :status=>"incomplete"}>
explain(name, task)
# New task 'and the trash' with status 'incomplete' has been added
name = "Take out the papers"
task = List.create_task(name)
#=> nil
explain(name, task)
# 'Take out the papers' is already in the list of tasks
List.tasks
#=> {"Take out the papers"=>#<Task:0x007f802305cf30
# #task={:name=>"Take out the papers", :status=>"real soon now"}>,
# "and the trash"=>#<Task:0x007f8023045088
# #task={:name=>"and the trash", :status=>"incomplete"}>}
List.tasks["Take out the papers"].task[:status]
#=> "real soon now"
1 This has the drawback of having the name of each task in two places (in the list hash and in the corresponding task instance variable (hash). In general, such duplication is not good programming practice. More experienced coders could probably offer advice here.
Related
Is there a way to watch objects so that a block or lamba is run before and/or after calls on specific methods for that object? For example, something like this:
watch(lionel, :say_you, :before) do
puts '[before say_you]'
end
lionel.say_you()
# outputs [before say_you]
An important part of my requirement is that I don't want to monkey patch the object at all. There should be no changes to the object. I just want to watch it, not change it (Heisenberg would be proud).
I've written a module that sort of does what I describe. Unfortunately, it has some bad side-effects: it slows down the system and never cleans up its hash of object ids. So I wouldn't use it in production, but it shows the concept of watching an object without monkey patching it.
# watcher module
# (Adrian is one of The Watchmen)
module Adrian
#registry = {}
EVENT_IDS = {:before => :call, :after => :return}
# watch
def self.watch(obj, method, event_id, &handler)
# get event type
event = EVENT_IDS[event_id]
event or raise 'unknown-event: unknown event param'
# get object id
obj_id = obj.object_id
# store handler
#registry[obj_id] ||= {}
#registry[obj_id][method] ||= {}
#registry[obj_id][method][event] ||= []
#registry[obj_id][method][event].push(handler)
end
# trace
TracePoint.trace(:call, :return) do |tp|
# get watched object or return
if handlers = #registry[tp.binding.receiver.object_id]
handle tp, handlers
end
end
# handle
def self.handle(tp, handlers)
# $tm.hrm
# if this method is watched
callee = handlers[tp.callee_id]
callee or return
# get blocks
blocks = callee[tp.event]
blocks or return
# loop through series
blocks.each do |block|
block.call
end
end
end
# Lionel class
class Lionel
def say_you
puts 'you'
end
def say_me
puts 'me'
end
end
# instance
lionel = Lionel.new()
# before
Adrian.watch(lionel, :say_you, :before) do
puts '[before say_you]'
end
# after
Adrian.watch(lionel, :say_me, :after) do
puts '[after say_me]'
end
# call method
lionel.say_you
lionel.say_me
That outputs:
[before say_you]
you
me
[after say_me]
Here is a solution making use of the singleton class for the object you want to watch. I am not sure it's good solution, but it might be at least interesting.
The watch method takes, and object, a method name, and an order parameter along with a block. The order parameter specifies when you want to run the block, ie before, after or around the watched method.
The watch method saves the original instance method, then defines a singleton method with the same name as the original method on the object. The singleton method calls the passed block at the time or times specified by the order parameter. The singleton method also calls with the original instance method bound to the object at the appropriate time.
Perhaps there is a better way to call an instance method from with in a singleton method, but I have not found one. I am also not sure how this would work with various argument types, ie keyword args, etc.
Here is the watch method.
def watch(obj,method_name,order,&block)
inst_method = obj.class.instance_method(method_name)
obj.define_singleton_method(method_name) do |*args|
block.call if [:before,:around].include?(order)
inst_method.bind(self).call(*args)
block.call if [:after,:around].include?(order)
end
end
Usage examples:
class Foo
def initialize(type)
#type = type
end
def i_method(arg)
puts "#{#type} instance method called with arg = #{arg}"
end
end
watched_inst = Foo.new("watched")
unwatched_inst = Foo.new("unwatched")
watch(watched_inst,:i_method,:before) do
puts "do before"
end
watched_inst.i_method("foo")
unwatched_inst.i_method("bar")
watched_inst.i_method("foo_bar")
watch(watched_inst,:i_method,:after) do
puts "do after"
end
watched_inst.i_method("hello")
watch(watched_inst,:i_method,:around) do
puts "do around"
end
watched_inst.i_method("hello")
unwatched_inst.i_method("foo")
output:
do before
watched instance method called with arg = foo
unwatched instance method called with arg = bar
do before
watched instance method called with arg = foo_bar
watched instance method called with arg = hello
do after
do around
watched instance method called with arg = hello
do around
unwatched instance method called with arg = foo
Output: <'class:List'> uninitialized constant List::Task (NameError)
You have created a new list
What I think is happening is that when I call Task.new, the List class is looking for possibly a task method or variable within its own class.
So far I tried using include Task and require Task in my List class with no luck. I also tried to declare the List class in my Task class. I also tried making the list class a parent of the Task class. After some digging online I thought it was the Ruby version and even changed the PATH to an older ruby version.
class List
attr_reader :all_tasks
if __FILE__ == $PROGRAM_NAME
my_list = List.new
puts 'You have created a new list'
my_list.add(Task.new('Make breakfest'))
puts 'You added a task'
end
def initialize
#all_tasks = []
end
def add(task)
all_tasks << task
end
end
class Task
attr_reader :description
def initialize(description)
#description = description
end
end
It might have helped if you provided more information about where (like: in which files) the classes are defined and how you call/execute your ruby code.
Usually, you would put code in a lib directory, with one class per file and strong semantics (class List goes into lib/list.rb), but its a tiny bit more complicated than that.
If you want to hack around and learn play a bit with ruby it is also perfectly understandable, that you do not want to cope with requireing (loading) other files, dealing with dependencies and all that (although this will need to happen one day as with most other programming languages).
For now, this would fix your issue and should get you going:
# file name: task_list_program.rb (or anything you want)
class Task
attr_reader :description
def initialize(description)
#description = description
end
end
class List
attr_reader :all_tasks
def initialize
#all_tasks = []
end
def add(task)
all_tasks << task
end
end
# note that for playing around in a single file, you actually
# do not even need the 'if ...' part here
if __FILE__ == $PROGRAM_NAME
my_list = List.new
puts 'You have created a new list'
my_list.add(Task.new('Make breakfest'))
puts 'You added a task'
end
The main "trick" here is to move the if __FILE__ == ... thing out of your class definition, because otherwise (you are accidentally dealing with a special case here), stuff evaluated in that class at runtime will not be able to pick up and reference other "definitions" in the way you seem to expect.
I hope this helps you to get going. The differences between the code example could teach you a lot, e.g. about "namespaces" (which afaik are not a real concept in Ruby), but I believe this short answer is good enough to get you started and have some fun with ruby (and welcome at SO by the way)!
("execute" the file like this: ruby task_list_program.rb).
class List
class Task
attr_reader :description
def initialize(description)
#description = description
end
end
attr_reader :all_tasks
if __FILE__ == $PROGRAM_NAME
my_list = List.new
puts 'You have created a new list'
List::Task.new('Make breakfest')
# my_list.add(Task.new('Make breakfest'))
puts 'You added a task'
end
def initialize
#all_tasks = []
end
def add(task)
all_tasks << task
end
end
The first issue what we had in this file, that we try to call Task class before implemented!
You don't correct call Task class my_list.add(Task.new('Make breakfest'))
correct is List::Task.new('Make breakfast')
I have two classes List and Task:
class List
attr_reader :all_tasks
def initialize
#all_tasks = []
end
def add (task)
#all_tasks << task
end
def show
all_tasks
end
end
class Task
attr_reader :description
def initialize (description)
#description = description
end
end
And the following code:
breakfast = Task.new("Make Breakfast")
my_list = List.new
my_list.add(breakfast)
my_list.add(Task.new("Wash the dishes!"))
my_list.add("Send Birthday Gift to Mom")
puts "Your task list:"
puts my_list.show
Output:
Your task list:
#<Task:0x00007fd9e4849ed0>
#<Task:0x00007fd9e4849e30>
Send Birthday Gift to Mom
I want to be able to show the tasks of the to-do list as a string and in the same time have the Task instances as objects inside the array. How do I do that?
Considering the code in your question, it would suffice to simply redefine method to_s of Task.
class Task
attr_reader :description
def initialize (description)
#description = description
end
def to_s
"Task: #{description}"
end
end
Output
Your task list:
Task: Make Breakfast
Task: Wash the dishes!
Send Birthday Gift to Mom
You call add with Task instances:
my_list.add(Task.new("Wash the dishes!"))
and with String instances:
my_list.add("Send Birthday Gift to Mom")
Having a mixture of both, Task and String instance in one array makes it harder to work with. Unless you really want or need this, I would change add, so it converts string arguments to Task instances:
class List
# ...
def add(task)
task = Task.new(task) unless task.is_a? Task
#all_tasks << task
end
end
is_a? checks whether task is (already) a Task. If not, it is passed as an argument to Task.new which returns such instance. This ensures, that #all_tasks only contains Task instances.
Your current implementation of List#show simply returns all_tasks, i.e. an array. Although puts is able to print arrays ...
If called with an array argument, writes each element on a new line.
... I would change show to return a formatted string:
class List
# ...
def show
all_tasks.map { |task| "[ ] #{task.description}" }.join("\n")
end
end
map returns a new array with a string for each task instance. Each string contains the corresponding task's description, prefixed by [ ] which should resemble a little check box. join then concatenates these string elements using "\n" (newline) as a separator.
Output:
Your task list:
[ ] Make Breakfast
[ ] Wash the dishes!
[ ] Send Birthday Gift to Mom
I'm still extremely new to Ruby. I took a class on Codecademy and I'm currently doing the "Final" where I have to make a todo list.
One of the parts of the todo list is to be able to add tasks (obviously). Another part is to be able to show all current tasks. Now, technically, both of these are working. But, when I create a new task using the class I made (Task) and then show the tasks, it displays the object ID instead of the string. If I simply use my add method without using my Task class, it will display the string like I want it too.
My goal is to get my script to display the string while using the Task class. If someone could please explain to me why it's not working and how I can fix it, I'd appreciate that.
Here's the code:
## Classes ##
#List Class - Used for anything involving the list
class List
attr_reader :all_tasks
def initialize
#all_tasks = []
end
def add(task)
all_tasks << task
end
def show
all_tasks
end
end
#Task Class - Used for anything involving Tasks
class Task
attr_reader :description
def initialize(description)
#description = description
end
end
## Modules ##
module Promptable
def prompt(message = "What would you like to do?", symbol = " >: ")
print message
print symbol
gets.chomp
end
def show
menu
end
end
module Menu
def menu
puts "
'add' - Add a task to the list \n
'delete' - Delete a task from the list \n
'update' - Update a task in the list \n
'show' - Shows current tasks in list"
end
end
#Methods - various methods
#Program Runner
if __FILE__ == $PROGRAM_NAME
include Menu
include Promptable
my_list = List.new
puts "Please choose from the following list: "
until ['q'].include?(user_input = prompt(show).downcase)
case user_input
when 'add'
puts "What task would you like to do?"
my_list.add(Task.new(gets.chomp))
when 'q'
puts "Qutting...."
when 'show'
puts my_list.show
else "That is not a valid command"
end
end
end
puts my_list.show
Will display the tasks one by one. Since the Task class doesn't have a to_s method, the default one will be used. Just add one:
class Task
# ...
alias to_s description
end
BTW strings are objects too. Pretty much everything in Ruby is an object.
I have one class, that in its methods produce an output of a hash.
=> {"A"=>"1", "B"=>"2"}
My question is how can I send this produced hash to another class, with methods that will further process this hash data?
Ive read the ruby doc and searched on StackOverflow, but can't seem to figure out how to get this new class to pick up this data (from the original class).
I am getting "undefined local variable or method" error when attempting to call on the methods of the first class, while in the second class.
Thanks for your help on this.
Hopefully I supplied enough background on my issue that someone can provide some guidance.
EDIT--
Here is the code that I have, which produces a this above mentioned hash. Actually at this stage it's an array.
Ideally I would like to have all the code from build_list method, on downwards, to be in a totally separate class. Doing this would require me to pass the produced array data (from user input) to these other methods, in this new class. In this new class I would like to have the completed playlist printed. So I would like to spit this example code into two classes, with the second class doing all of the processing work on the user supplied artists. I hope this is clearer.
require 'lib/uri.rb'
require 'json'
require 'rest_client'
require 'colorize'
class Playlist
attr_accessor :artistInput, :artist_list
def initialize
#artistInput = artistInput
#artist_list = []
#artist = #artist
end
def self.start #welcome to the application + rules
puts "-------------------------------------------------------------------------------------------------------------------"
puts "|This Playlist Creator application will create you a static playlist from a given list of artists supplied by you.|".colorize(:color => :white, :background => :black)
puts "-------------------------------------------------------------------------------------------------------------------"
puts "Hint: To stop adding artists, type '\N\' + the [enter] key at any time."
# puts "Hint: For artists with spaces, eg \'Lady Gaga\' - please remove the spaces and write as such \'LadyGaga\'."
end
system "clear"
def artistInput #loop that creates an artist_list array of an infinate amount of artists, stops at the keyword 'N'
#artist_list = []
while #artist != ""
puts "\nWhat artist would you like to add to the Playlist?"
#artist = gets.gsub(/\s+/, "")
if #artist != 'N'
puts "You have chosen to add #{#artist} to the Playlist."
end
break if #artist =~ /N/
#artist_list << #artist.gsub(/\s+/, "") #.gsub removes spaces. 'Lady Gaga' => 'LadyGaga'
end
def build_list
##artist_list
list = #artist_list
list.join('&artist=')
end
def build_url_string
string = build_list
url = 'http://developer.echonest.com/api/v4/playlist/static?api_key=G3ABIRIXCOIGGCCRQ&artist=' + string
end
system "clear"
def parse_url
url = build_url_string
response = JSON.parse(RestClient.get url)
song = response['response']['songs'].each do |song|
song.delete("artist_id")
song.delete("id")
puts
song.each {|key, value| puts "#{key.colorize(:white)}: \'#{value.colorize(:color => :white, :background => :black)}\'" }
end
# a = response['response']['songs']
# a.fetch('id')
end
a = parse_url
puts "\nThere are #{a.length} songs in this Playlist\n\n"
#Uncomment this section to see the raw data feed
# puts "////////////////raw data hash feed//////////////"
# puts "#{a.to_s}"
end
end
Its going to be really difficult to answer your question without code samples. But I am going to try my best with this example
class A
def get_hash
{"A"=>"1", "B"=>"2"}
end
end
class B
def process_hash(hash)
#do something with the hash
end
end
hash = A.new.get_hash
B.new.process_hash(hash)