I'd like to create a container class for objects based on a Ruby array. I'd like to manipulate more than one of these containers, like concatenating 2 together. If I try this:
class Thing
attr_accessor :name
end
class Things
def initialize
#things = Array.new
end
def addone( a )
#things.push( a )
end
def append( list )
list.each { |i| addone( i ) }
end
end
item1 = Thing.new
item2 = Thing.new
item3 = Thing.new
item4 = Thing.new
item1.name = "Marty"
item2.name = "Fred"
item3.name = "Janice"
item4.name = "John"
list1 = Things.new
list1.addone( item1 )
list1.addone( item2 )
list2 = Things.new
list2.addone( item3 )
list2.addone( item4 )
list3 = Things.new
list3 = list2.append( list1 )
I get the error:
in append': undefined methodeach' for # (NoMethodError) from ./test.rb:40:in `'
I've tried different approaches, for example creating an each method as it seems to want, but no luck so far. Any suggestions? And thanks in advance!
If you want to be able to add Things to Things, you have two abilities: either to implement iterator methods on Things or simply decorate wrapped Array:
def append(list)
case list
when Enumerable then list.each { |i| addone(i) }
when Things then list.instance_variable_get(:#things).each { |e| addone(i) }
else raise "Sorry, can’t add #{list}"
end
I guess there should be a getter/setter methods:
attr_accessor :things
Then you should change your addone method:
def append(list)
list.things.each { |i| addone( i ) } # iterate through array items, not Things instance object
self # return appended list object instead of unchanged provided argument – list1
end
Output of list3.things:
=> [#<Context::Thing:0x00000001adea48 #name="Janice">,
#<Context::Thing:0x00000001ade9f8 #name="John">,
#<Context::Thing:0x00000001adea98 #name="Marty">,
#<Context::Thing:0x00000001adea70 #name="Fred">]
Demonstration
Consider this approach:
class Thing
attr_accessor :name
def initialize(name)
#name = name
end
end
class Things
def initialize(things = [])
#things = things
end
def push(thing)
#things.push(thing)
end
def append(other)
#things << other.to_a
end
def +(other)
Things.new(#things + other.to_a)
end
def to_a
#things
end
end
some_things = %w(Marty Fred Janice John).map { |name| Thing.new(name) }
things_1 = Things.new
some_things.first(2).each { |thing| things_1.push(thing) }
things_2 = Things.new
some_things.last(2).each { |thing| things_2.push(thing) }
things_1.append(things_2) # This actually appends to things_1 rather than creating a new object
new_things = things_1 + things_2 # Creates a new object
# => #<Things:0x007ff85a1aa770 #things=[
# #<Thing:0x007ff85a1aa928 #name="Marty">,
# #<Thing:0x007ff85a1aa900 #name="Fred">,
# #<Thing:0x007ff85a1aa8d8 #name="Janice">,
# #<Thing:0x007ff85a1aa8b0 #name="John">]>
Notes:
Modified the API a bit to simplify the code.
Added a new method + as its intuitive in this context.
Related
I have a JSON data structure like this...
{
"items": [
{
"person": { // person hash }
},
{
"dog": { // dog hash }
},
{
"fruit": { // fruit hash }
},
{
“person”: { // person hash }
}
]
}
}
Each item in the array contains only one key:value pair. The key is the bot that tells me what type of item the value is.
What I'd like to do is iterate the array and run a different function for each type of item.
So I have something like this...
items = data.dig('items')
items.map do |item|
if person = item.dig('person')
transform_person(person)
elsif dog = item.dig('dog')
transform_dog(dog)
elsif fruit = item.dig('fruit')
transform_fruit(fruit)
end
end
But I feel like there should be a more elegant way to do this?
Apologies. I appear to have left some ambiguity in my question.
The initial array may contain multiple items with the same key. What I am trying to do is map to an array of items that are transformed into what is required by the front end. The input contains a strange structure and info that is not needed by the front end.
So the output array order must match the input array order.
Sorry for the confusion.
First you'll want to define the key preference in a constant:
PECKING_ORDER = %w[ person dog fruit ]
Then you can use that to find it:
def detect(item)
PECKING_ORDER.lazy.map do |key|
[ key, item.dig(key) ]
end.find do |key, v|
v
end
end
Where that can dig up the first item that's found. lazy is used here so it doesn't dig them all up needlessly, just does them one at a time until there's a hit.
This gives you a key/value pair which you can use with dynamic dispatch:
items.each do |item|
key, value = detect(item)
if (key)
send(:"transform_#{key}", value)
end
end
if you know the mapping, you could make a pseudo factory hash:
methods_mapped = {
"person" => ->(person) { do_something_with_person(person) },
"dog" => ->(dog) { do_something_with_dog(dog) },
"fruit" => ->(fruit) { do_something_with_fruit(fruit) }
}
items.map do |item|
key = item.keys.first # what if keys.size > 1 ?
method = methods_mapped.fetch(key)
method.call(item[key])
end
or you could it from the opposite direction:
methods_mapped.each do |key, method|
method.call(items.dig(key))
end
Let f be a given method that takes as an argument a hash. Without loss of generality, suppose it is as follows. This corresponds to the OP's transform_person, transform_dog and transform_fruit methods combined.
def f(h)
case h.keys.first
when :person then "somebody"
when :dog then "doggie"
when :fruit then "juicy"
end
end
Suppose we are also given (no need for dig here)
items = data[:items]
#=> [{:person=>{:name=>"Melba"}},
# {:dog=>{:tricks=>[:roll_over, :shake_a_paw]}},
# {:fruit=>{:good=>"raspberries"}}]
and
key_order = [:bird, :marsupial, :dog, :person]
We wish to find the first element k of key_order for which items contains a hash h for which h.key?(k) #=> true. If such a hash h is found we are to then execute f(h).
First compute a hash key_map.
key_map = items.each_with_object({}) { |g,h| h[g.keys.first] = g }
#=> {:person=>{:person=>{:name=>"Melba"}},
# :dog=>{:dog=>{:tricks=>[:roll_over, :shake_a_paw]}},
# :fruit=>{:fruit=>{:good=>"raspberries"}}}
Then we simply execute
k = key_order.find { |k| key_map[k] }
#=> :dog
k ? f(key_map[k]) : nil
#=> "doggie"
I would kept it simple:
items.map do |item|
do_something_with_person(item) if item.dig('person')
do_something_with_dog(item) if item.dig('dog')
do_something_with_fruit(item) if item.dig('fruit')
end
or
items.each do |item|
case item
when item.dig('person') then do_something_with_person(item)
when item.dig('dog') then do_something_with_dog(item)
when item.dig('fruit') then do_something_with_fruit(item)
end
end
or
def do_something(item)
case
when item.dig('person') then do_something_with_person(item)
when item.dig('dog') then do_something_with_dog(item)
when item.dig('fruit') then do_something_with_fruit(item)
end
end
items.map { |item| do_something(item) }
I have tried at least 5 different ways to do it, currently using the .each method.
I believe the problem lies in the print_songs method at the bottom of the code block.
The error I'm getting:
Artist #print_songs lists all of the artist's songs
Failure/Error: expect{artist.print_songs}.to output("Dirty Diana\nBillie Jean\n").to_stdout
expected block to output "Dirty Diana\nBillie Jean\n" to stdout, but output "#<Song:0x000000019757b0>\n#<Song:0x000000019756e8>\n"
Diff:
## -1,3 +1,3 ##
-Dirty Diana
-Billie Jean
+#<Song:0x000000019757b0>
+#<Song:0x000000019756e8>
The code:
class Artist
attr_accessor :name, :song
##all = []
def initialize(name)
#name = name
#songs = []
end
def add_song(song)
#songs << song
end
def songs
#songs
end
def self.all
##all
end
def save
self.class.all << self
end
def self.create_by_name(name)
artist = Artist.new(name)
end
def self.find_or_create_by_name(name)
artist_name = ##all.detect{|x| x.name == name}
if artist_name == nil
self.create_by_name(name)
else
artist_name
end
end
def print_songs
#songs.each{|song| puts song}
end
end
Song Class:
class Song
attr_accessor :name, :artist
def initialize(name)
#name = name
end
# def artist=(name)
# self.artist = Artist.new(name)
# end
def self.new_by_filename(file_name)
file_name.basename("")
end
end
Answer:
def print_songs
#songs.each{|song| puts song.name}
end
It's apparent that the song is probably just not a string, but an object of some sort. Add a to_s method to whatever object that is and puts should automatically call that, although you could, of course, also call to_s manually.
Here's the quick test I did to confirm this behaviour
irb(main):001:0> class Song
irb(main):002:1> def initialize(title)
irb(main):003:2> #title = title
irb(main):004:2> end
irb(main):005:1> end
=> :initialize
irb(main):006:0> s = Song.new "scarborough fair"
=> #<Song:0x0000000030bb78 #title="scarborough fair">
irb(main):007:0> puts s
#<Song:0x0000000030bb78>
=> nil
irb(main):008:0> class Song
irb(main):009:1> def to_s
irb(main):010:2> return #title
irb(main):011:2> end
irb(main):012:1> end
=> :to_s
irb(main):013:0> puts s
scarborough fair
=> nil
irb(main):014:0>
EDIT:
But why is this?
In short, when you create some custom object class, like Song in my example above, Ruby has no idea how to represent it as a string. The default to_s method simply outputs the class and the object ID, which is OK for debugging, but if there's some obvious way to represent the object as a string, like in the case of Song (one would expect song.to_s to return the song title), one has to override the default to_s method or include / inherit from another class/module that has a more fitting implementation of to_s.
It is working as expected.Lets look at tried.Could you post the test cases you have tried with?
a1 = Artist.create(name: 'Lana del re')
a1.add_song('Young and Beautiful')
a1.add_song('haunted')
a1.print_songs
["Young and Beautiful", "haunted"] #output assuming song as a String object
By default, the call to to_s prints the object's class name and an encoding of the object id like so: "#<Song:0x007f9fd16a0770>". That said, you need to override the to_s method on a Song class:
class Song
def initialize(title)
#title = title
end
def to_s
#title
end
end
Then you would need to modify the print_songs method accordingly on an existing Artist class:
def print_songs
puts #songs.each(&:to_s)
end
The each(&:to_s) bit basically invokes the to_s method on each Song object passed to the block, which is essentially the same as
def print_songs
puts #songs.each { |song| song.to_s }
end
This could've also been rewritten as just
def print_songs
puts #songs
end
and in this case puts would implicitly invoke the to_s method on the elements of #songs.
So if a have this code:
class A
def initialize(type)
#type = type
end
end
instance = A.new(2)
another_instance = A.new(1)
array = [instance, another_instance]
is there a way to check if array includes an instance of A where #type is equal to a certain value? say, 2? like the include? method but where instead of checking for an instance of a certain class, it also checks the instance variables of that class?
I would recommend using anattr_reader for this one unless you plan on modifying the type somewhere after (in that case use attr_accessor which is both a writer and reader)
class A
attr_reader :type
def initialize(type)
#type = type
end
end
instance = A.new(2)
another_instance = A.new(1)
array = [instance, another_instance]
array.select do |item|
item.type == 2
end
=>[#<A:0x00000000dc3ea8 #type=2>]
Here I am iterating through an array of instances of A and selecting only the ones that meet the condition item.type == 2
You can just refer to the instance variable.
> array.any? { |item| item.is_a?(A) }
=> true
> array.any? { |item| item.instance_variable_get(:#type) == 1 }
=> true
> array.select { |item| item.instance_variable_get(:#type) == 1 }
=> [#<A:0x007fba7a12c6b8 #type=1>]
Or, use attr_accessor in your class, to make it way easier
class A
attr_accessor :type
def initialize(type)
#type = type
end
end
then you can do something = A.new(5); something.type
I am by no means an expert, and I've run into a little snag when handling objects via a Readline input.
I have a Player class, an Inventory class, and an Item class, and I want the player to be able to type "wear brick" and have it match an Item.name within the array Inventory.contents and add that item to Player.worn. Here's what I have:
class Player
def wear(item)
#worn << item
end
end
class Inventory
attr_accessor :contents
def initialize
#contents = []
#stick = Item.new('a brown stick', 'long and pointy')
#brick = Item.new('a red brick', 'rough and heavy')
#contents << #stick
#contents << #brick
end
def list_contents
#contents.each {|item| puts item.name }
nil
end
end
class Item
attr_accessor :name, :desc
def initialize(name, desc)
#name = name
#desc = desc
end
end
player = Player.new
inv = Inventory.new
I have a Readline prompt to accept the first token of an input string as a command, here 'wear' as a case, but I'm having a hard time grasping how to match the subsequent string within a object inside an array of objects with one word.
inv.contents.inspect:
[#<Item:0x007fcf61935bb0 #name="a brown stick", #desc="long and pointy">, #<Item:0x007fcf61935b38 #name="a red brick", #desc="rough and heavy">]
How could I have "wear brick" search through the Array inv.contents to match #name="a red brick" and select that item to input into Player.wear?
#c650 : thank you. that looks to have solved it!
New Class:
class Player
def initialize
#inventory = Inventory.new
#worn = []
#input = "brick"
end
def wear(keyword)
#inventory.contents.map{|x| #worn << x and #inventory.contents.delete(x) if x.name.include?(keyword) }
end
end
I'd like to delete an item in my loop
i have a array of instance of my class, and i need sometimes to delete this items from my array
class Test
attr_reader :to_del
def initialize(str)
#to_del = str
end
end
tab = Array.new
a = Test.new(false)
b = Test.new(true)
c = Test.new(false)
tab.push(a)
tab.push(b)
tab.push(c)
for l in tab
if l.to_del == true
l = nil
end
end
p tab
any idea ?
For inplace deletion:
tab.reject! { |l| l.to_del }
to return just a cleared array:
tab.reject &:to_del
The whole code is php-smelled. I would go with:
tab = (1..3).map { [true,false].sample }.map { |e| Test.new e }
tab.reject &:to_del
You can use Array#delete_if.
Check this out:
tab
#=> [#<Test:0x00000007548768 #to_del=false>, #<Test:0x000000074ea348 #to_del=true>, #<Test:0x000000074b21a0 #to_del=false>]
tab.delete_if {|x| x.to_del}
#=> [#<Test:0x00000007548768 #to_del=false>, #<Test:0x000000074b21a0 #to_del=false>]