How to iterate over an array of objects Ruby - arrays

I created a class and added objects like so:
class Tops
def initialize(neckline, brand, maincolour, style)
#neckline = neckline
#brand = brand
#maincolour = maincolour
#style = style
end
end
top1 = Tops.new("Sweetheart", "Miss Selfridge", "Blue", "Bardot")
top2 = Tops.new("Scoop", "Target", "Pink", "Vest")
top3 = Tops.new("Boat", "Unknown", "Red", "Tank")
I then used the push method to add some of these objects into an empty array.
currentTopChoices = []
currentTopChoices.push(top1,top3)
I then used some the each method to run a block of code or each object in this array
currentTopChoices.each { |x| puts #neckline }
So my aim was really to get an output of each neckline of the objects in the array but I get an output of:
Boat
Boat
When I really wanted:
Sweetheart
Boat
Which is really the neckline of the last object but printed twice. I get no error messages so what exactly am I missing here?

If you don't mind adding an attr_accessor to Tops (which will provide a Top#neckline accessor method), you can use Array#map or (its alias Array#collect) to build a collection of necklines:
class Tops
attr_accessor :neckline
def initialize(neckline, brand, maincolour, style)
#neckline = neckline
#brand = brand
#maincolour = maincolour
#style = style
end
end
top1 = Tops.new("Sweetheart", "Miss Selfridge", "Blue", "Bardot")
top2 = Tops.new("Scoop", "Target", "Pink", "Vest")
top3 = Tops.new("Boat", "Unknown", "Red", "Tank")
[top1, top3].map(&:neckline) # => ["Sweetheart", "Boat"]
# or, more explicitly
[top1, top3].map { |top| top.neckline } # => ["Sweetheart", "Boat"]

The problem is that somehow you are confusing yourself by either not showing all your code, or maybe running this in irb, where at some point you had a statement like?
#neckline = "Boat"
For example the code you posted, if placed directly in a file_name.rb like so:
class Tops
def initialize(neckline, brand, maincolour, style)
#neckline = neckline
#brand = brand
#maincolour = maincolour
#style = style
end
end
top1 = Tops.new("Sweetheart", "Miss Selfridge", "Blue", "Bardot")
top2 = Tops.new("Scoop", "Target", "Pink", "Vest")
top3 = Tops.new("Boat", "Unknown", "Red", "Tank")
currentTopChoices = []
currentTopChoices.push(top1,top3)
currentTopChoices.each { |x| puts #neckline }
and then run using ruby file_name.rb
would just produce 2 blank lines because #neckline is not define at this scope (toplevel), so it's value is nil
To get this to work the way you intend you can add
attr_reader :neckline
and then refer to x.neckline within the block you pass to each, since x will take one the value of each of the Tops objects in currentTopChoices
note: attr_accessor adds both a setter and a getter for neckline, if you only need read access to that property than that's all you should allow (which is what you get with attr_reader ), if you really need to both get and set the value of neckline on an instance after creating it then att_accessor would make sense.
Here is an updated version of your code that should behave as you are expecting.
class Tops
attr_reader :neckline # added to allow read access to #neckline
def initialize(neckline, brand, maincolour, style)
#neckline = neckline
#brand = brand
#maincolour = maincolour
#style = style
end
end
top1 = Tops.new("Sweetheart", "Miss Selfridge", "Blue", "Bardot")
top2 = Tops.new("Scoop", "Target", "Pink", "Vest")
top3 = Tops.new("Boat", "Unknown", "Red", "Tank")
currentTopChoices = []
currentTopChoices.push(top1,top3)
currentTopChoices.each { |x| puts x.neckline } # access the neckline property for each element

Related

How can I create instances of a class with data from a nested array?

I am creating a project where I have scraped data from a webpage for product info.
My scraping method returns a nested array with a collection of product_names, urls, and prices. I'm trying to use the nested array to create instances of my class Supplies, with attributes of a name, url, and price. The nested array has the same number of elements in each of the 3 array.
I want to ##all to return an array of instances of all products in the collection with their attributes set.
name_url_price = [[],[],[]]
class Catalog::Supplies
attr_accessor :name, :price, :url
##all = []
def initialize(name_url_price)
count = 0
while count <= name_url_price[0].length
self.name = name_url_price[0][count]
self.url = name_url_price[1][count]
self.price = name_url_price[2][count]
##all << self
count += 1
end
end
There's a lot going wrong here but nothing that can't be fixed:
Storing all the instances in a class variable is a little strange. Creating the instances and having the caller track them would be more common and less confusing.
Your while count <= ... loop isn't terminated with an end.
Your loop condition looks wrong, name_url_price[0] is the first element of name_url_price so name_url_price[0].length will, presumably, always be three. If name_url_price looks more like [ [names], [urls], [prices] ] then the condition is right but that's a strange and confusing way to store your data.
while loops for iteration are very rare in Ruby, you'd normally use name_url_place.each do ... end or something from Enumerable.
Your array indexing is possibly backwards, you want to say name_url_price[count][0], name_url_price[count][1], ... But see (3) if I'm misunderstanding how your data is structured.
Your ##all << self is simply appending the same object (self) to ##all over and over again. ##all will end up with multiple references to the same object and that object's attributes will match the last iteration of the while loop.
The initialize method is meant to initialize a single instance, having it create a bunch of instances is very strange and confusing.
It would be more common and generally understandable for your class to look like this:
class Catalog::Supplies
attr_accessor :name, :price, :url
def initialize(name, url, price)
self.name = name
self.url = url
self.price = price
end
end
And then your name_url_price array would look more like this:
name_url_price = [
[ 'name1', 'url1', 1 ],
[ 'name2', 'url2', 2 ],
[ 'name3', 'url3', 3 ],
[ 'name4', 'url4', 4 ]
]
and to get the supplies as objects, whatever wanted the list would say:
supplies = name_url_price.map { |a| Catalog::Supplies.new(*a) }
You could also use hashes in name_url_price:
name_url_price = [
{ name: 'name1', url: 'url1', price: 1 },
{ name: 'name2', url: 'url2', price: 2 },
{ name: 'name3', url: 'url3', price: 3 },
{ name: 'name4', url: 'url4', price: 4 }
]
and then create your instances like this:
supplies = name_url_price.map do |h|
Catalog::Supplies.new(
h[:name],
h[:url],
h[:price]
)
end
or like this:
supplies = name_url_price.map { |h| Catalog::Supplies.new(*h.values_at(:name, :url, :price)) }

Is there a better way to restructure this array in Ruby?

I've got an array that looks like this:
{
year_list: [{
name: 2016,
make_list: [{
name: 'Honda',
model_list: [{
name: 'CRV',
series_list: [{
name: 'Premium Plus',
style_list: [{
name: '4D SUV',
uvc: '123abc'
}]
}]
}]
}]
}]
}
And I want to transform this array to the following, where each combination of year, make, model, series and style combined into a hash and inserted into array.
[{:year=> 2016,
:make=>"Honda",
:model=>"CRV",
:series=>"Premium Plus",
:style=>"4D SUV",
:uvc=>"123abc"}]
I have a working solution, but I'm wondering if anyone has any ideas on how to make the solution more elegant solution.
list = []
year_list.each do |y_val|
vehicle_type = {}
vehicle_type[:year] = y_val['name']
make_list = y_val['make_list']
make_list.each do |k_val|
vehicle_type[:make] = k_val['name']
model_list = k_val['model_list']
model_list.each do |m_val|
vehicle_type[:model] = m_val['name']
series_list = m_val['series_list']
series_list.each do |s_val|
vehicle_type[:series] = s_val['name']
style_list = s_val['style_list']
style_list.each do |st_val|
vehicle_type[:style] = st_val['name']
vehicle_type[:uvc] = st_val['uvc']
list << vehicle_type
end
end
end
end
end
Is there a way to utilize some of Ruby's array methods to make this a better solution?
Edit: Here's a solution that is almost working using recursion.
VEHICLES = []
ELEMENTS = [:year, :make, :model, :series, :style]
def walk_tree(arr, vehicle={})
arr[0..1].each do |a|
name = a['name']
if a.key?('uvc')
vehicle[:uvc] = a['uvc']
VEHICLES << vehicle
vehicle = {}
next
end
ELEMENTS.each do |s|
key = s.to_s + '_list'
if a.key?(key)
vehicle[s] = name
walk_tree(a[key], vehicle)
end
end
end
end
walk_tree(year_list)
pp LIST
It's possible to rewrite your solution without any additional accumulators or temporary variables which will shrink the number of code lines and make it clear. My solution uses only Array map method which will return desired result, but deeply wrapped into arrays. All external array wrappers can be removed with Array flatten method call.
Here is the output from irb console.
super_hash[:year_list].map do |year|
year[:make_list].map do |make|
make[:model_list].map do |model|
model[:series_list].map do |series|
series[:style_list].map do |style|
{
year: year[:name],
make: make[:name],
model: model[:name],
series: series[:name],
style: style[:name],
uvc: style[:uvc]
}
end
end
end
end
end
#=> [[[[[{:year=>2016, :make=>"Honda", :model=>"CRV", :series=>"Premium Plus", :style=>"4D SUV", :uvc=>"123abc"}]]]]]
[[[[[{:year=>2016, :make=>"Honda", :model=>"CRV", :series=>"Premium Plus", :style=>"4D SUV", :uvc=>"123abc"}]]]]].flatten
#=> [{:year=>2016, :make=>"Honda", :model=>"CRV", :series=>"Premium Plus", :style=>"4D SUV", :uvc=>"123abc"}]
You may recursively inject, passing all key-values as is, but looking for _list ones like this:
l = ->(h,(k,v)){
if k =~ /(\w+)_list$/
h[$1] = v.first[:name]
return v.first.inject(h, &l)
end
h[k] = v
h
}
p year_list.inject({}, &l)

check if array includes an instance with a certain value for an instance variable Ruby

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

Add elements to an array using instance methods

I want to add elements to an array. Each time, elements are passed as parameters by calling a different method. That is, calling the following:
new_register = CashRegister.new
new_register.add_item("eggs", 1.99)
new_register.add_item("tomato", 1.76, 3)
should return ["eggs", "tomato", "tomato", "tomato"].
Here's my code in its entirety:
class CashRegister
attr_accessor :total, :discount, :title, :price, :array_of_all_items
def initialize(discount = 0)
#total = 0
#discount = discount
end
def add_item (title, price, quantity = 1)
#title = title
self.total = self.total + (price * quantity)
end
def apply_discount
if
discount == 0
"There is no discount to apply."
else
total_discount = self.total * (discount / 100.0)
self.total = self.total - total_discount
"After the discount, the total comes to $#{self.total.round}."
end
end
def items
array_of_all_items << self.title
end
end
Within def items method, I'm stumped on how to add items to the array without overriding the array that was there previously. Also confused about whether I needed to use attr_accessor to set and get the parameters that are passed to def add_item so that I can use them in def items.
If add items is meant to change the state of the cashregister, then it will change(override) the array. To add to an array you can use ruby's push method, or the shovel operator <<.
def add_items thing, price, number = 1
number.times { #log << thing }
or
number.times { #log.push thing }
return #log
end
unless you want the storage array instance variable accessible outside of the class, you do not need attr_*'s.

How do I reference another object of the same class in Ruby?

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.

Resources