Initializing multiple objects at once? - ruby

So, I'm trying to initialize multiple objects and add them to a list. What I'm wanting to happen is by running Market.new, I want every item from the api added as an object. Below is the code I thought might work. But, it's adding the same object to the list 100x. Is there a way to accomplish this?
def initialize
data = JSON.parse(open(BASE_URL + "markets?vs_currency=usd").read)
i = 0
# looping until we hit the end of the list. adding them all as objects.
while i < data.length
#id = data[i]["id"].to_s
#name = data[i]["name"].to_s
#symbol = data[i]["symbol"].to_s
#price = data[i]["current_price"].to_s
#price_movement_24h = data[i]["price_change_percentage_24h"].to_s
#market_cap = data[i]["market_cap"].to_s
##market << self
i += 1
end
end
This gives me the same object added to the ##market list 100x.
=> [#<Market:0x0000561b3f853f30
#id="iostoken",
#market_cap="43969067",
#name="IOST",
#price="0.0036523",
#price_movement_24h="-0.76702",
#symbol="iost">,
#<Market:0x0000561b3f853f30
#id="iostoken",
#market_cap="43969067",
#name="IOST",
#price="0.0036523",
#price_movement_24h="-0.76702",
#symbol="iost">,

I want to start by saying this is very strange Ruby code and not something you'd typically do. That's not meant as an insult, just to say that Ruby devs tend to follow the same or similar guidelines on structuring objects and this chunk of code feels like it's ported from another language.
The issue you're seeing is due to the fact that within the initialize method you're not creating any new objects but instead updating the instance variables and pushing self into a class variable. self is referencing this instance directly which means the class variable array is filling up with references to the same object. If you're adamant on keeping the code the same then you should instead push a duplicate of your object after you've updated the instance variables.
##market << self.dup
This creates a duplicate object that has a different memory address and reference.
If you're looking to write more idiomatic code you'd want to use multiple objects and not rely on class variables at all. If you're not interpolating a variable in a string use single quotes instead of double quotes. Keep object methods simple and focused on specific tasks. These are just a few things Ruby developers consider when writing code, but find what works best for you.
Take something like this for instance:
class Market
attr_accessor :id, :name, :symbol, :price, :price_movement_24h, :market_cap
def initialize(data = {})
#id = data['id'].to_s
#name = data['name'].to_s
#symbol = data['symbol'].to_s
#price = data['current_price'].to_s
#price_movement_24h = data['price_change_percentage_24h'].to_s
#market_cap = data['market_cap'].to_s
end
end
class ImportService
def self.from_api(url)
response = JSON.parse(open(url).read) || []
response.map { |data| Market.new(data) }
end
end
You could then call this as such:
#market_data = ImportService.from_api(BASE_URL + 'markets?vs_currency=usd')

When adding self to the ##market, you should change the code to this
##market << self.dup
However, I don't think it's a good practice to use a class variable here and add self to init an array of object. Instead, you should create a new class (for example MarketImporter)
class Market
attr_accessor :id, :name, :symbol, :price, :price_movement_24h, :market_cap
def initialize(data = {})
#id = data["id"].to_s
#name = data["name"].to_s
#symbol = data["symbol"].to_s
#price = data["current_price"].to_s
#price_movement_24h = data["price_change_percentage_24h"].to_s
#market_cap = data["market_cap"].to_s
end
end
class MarketImporter
attr_accessor :markets
def initialize
data = JSON.parse(open(BASE_URL + "markets?vs_currency=usd").read)
#markets = data.collect { |item| Market.new(item) }
end
end
Then you can init the collection by
MarketImporter.new

Related

How do I create a new instance of class using values from an existing class in Ruby?

I am trying to understand how to create a new instance of a class from within an existing class in Ruby. I found one example, on Stack Overflow, but it isn't clear how the values from the existing class get passed to the new instance. (If I am using the terminology right).
In the example below, I have two classes, RaceCar and RaceDriver. When initialized RaceDriver sets the name, experience=0, winnings=0 and makes sure there is no car assigned yet.
When initialized RaceCar sets the number of the car, the type, damage=0, racing_status :ready, and sets the driver.
What I want to do is create a method in the RaceDriver, set_driverscar, that creates a car for the driver, creating RaceCar.new but setting the driver to the name of the RaceDriver. It does not matter if it is in initialization or not, I am just trying to figure out.
How would I do this like in the example below?
I have been beating my head on this for a bit, and I must be missing something obvious but I just cannot figure it out OR find a good example that explains how to do this?
Can you help me with an answer so I can learn what I am missing here?
class RaceDriver
attr_accessor :name, :experience, :winnings, :car
def initialize(attrs = {})
set_name(attrs [:name])
#experience = 0
#winnings = 0
#car = nil
end
private
def set_name(obj)
obj == nil ? no_name : #name = :name
end
def no_name
raise 'There is no name for this driver'
end
end
class RaceCar
attr_accessor :racing_number, :type, :damage, :racing_status, :driver
def initialize(attrs = {})
#racing_number = generate_number
#type = "stock"
#damage = 0
#racing_status = :ready
set_driver(attrs[:driver])
end
def disable
#racing_status = :disabled
end
private
def generate_number
rand(20..99)
end
def set_driver(obj)
obj == nil ? no_driver : #driver = obj
end
def no_driver
raise "A car has to have a driver to compete!"
end
end

Dynamic Variables to access class methods in Ruby

Working in Ruby, we have to use a 3rd party Framework, which has a class setup something like this:
class Foo
attr_accessor :bar
def initialize()
end
end
class Poorly_Designed_Class
attr_accessor :thing1
attr_accessor :thing2
attr_accessor :thing3
attr_accessor :thing4
attr_accessor :thing5
# through :thing_n .. number defined at runtime
def initialize()
#thing1 = Foo.new
#thing2 = Foo.new
#thing3 = Foo.new
#thing4 = Foo.new
#thing5 = Foo.new
end
end
I don't know how many "things" there are until run time. there could be 5 or their could be 50.
What I would like to do is something like:
pdc = Poorly_Designed_Class.new
for i in 0..numberOfThings do
pdc."thing#{i}".bar = value[i]
end
The above doesn't work.
I've also tried accessing it via:
instance_variable_set("pdc.thing#{i}.bar",value)
I understand that the class should be using an array or hash. Unfortunately I can't do anything about how the class is designed and we have to use it.
Is what i'm trying to do even possible?
You could either try to call the getter (preferably, since it honors encapsulation):
pdc = PoorlyDesignedClass.new
1.upto(number_of_things.times do |i|
pdc.public_send(:"thing#{i}").bar = value[i]
end
or get the instance variable (less preferred, since it breaks encapsulation):
pdc = PoorlyDesignedClass.new
1.upto(number_of_things) do |i|
pdc.instance_variable_get(:"#thing#{i}").bar = value[i]
end
So, you were on the right track, there were just two problems with your code: instance variable names start with an # sign, and . is not a legal character in an identifier.
You're using Object#instance_variable_set incorrectly. The first argument must be a string or a symbol representing the name of an instance variable including the # prefix: e.g. "#thing{i}". However you actually want to get the value of an instance variable and then send #bar= to it. That can be done with Object#instance_variable_get:
1.upto(numberOfThings) { |i| pdc.instance_variable_get("#thing#{i}").bar = value[i] }
That's a bit long and since attr_acessor :thingX defines getter methods, it's usually preferable to call them with Object#public_send instead of directly accessing the instance variable (a getter method might do something else than just returning a value):
1.upto(numberOfThings) { |i| pdc.public_send("thing#{i}").bar = value[i] }

Error working with oop (classes)

I just started with ruby, and just started learning oop today, after making a class, I am trying to print to console yet I keep getting this error. Does anyone know what's wrong?
undefined method `set_brand_name=' for # (NoMethodError)
Here is the code causing this error:
class Laptop
def set_brand_name(brand_name)
#brand = brand_name
end
def get_brand_name
return #brand
end
def set_color(color)
#color = color
end
def get_color
return #color
end
def set_processor(processor)
#processor = processor
end
def get_processor
return #processor
end
def set_storage(hard_drive)
#storage = hard_drive
end
def get_storage
return #storage
end
def set_memory(ram)
#memory = ram
end
def get_memory
return #memory
end
end
my_laptop = Laptop.new
my_laptop.set_brand_name = "HP"
my_laptop.set_processor = 'i7-4700k'
my_laptop.set_memory = '16gb'
my_laptop.set_storage = '750gb'
my_laptop.set_color = 'Silver'
brand = my_laptop.get_brand_name
color = my_laptop.get_color
processor = my_laptop.get_processor
memory = my_laptop.get_memory
storage = my_laptop.get_storage
This should output the message:
"""The Laptop I want is an #{brand}, it has a #{processor},
#{memory} of ram, a #{storage}, and it #{color}!!!"""
What am I doing wrong?
The problem is that you are not calling the method names as you've defined them. You defined set_brand_name without an equal sign so use:
my_laptop.set_brand_name("HP")
I would simply the getters and setters like so:
class Laptop
def brand_name=(brand_name)
#brand_name = brand_name
end
def brand_name
#brand_name
end
end
Or even better:
class Laptop
attr_accessor :brand_name
end
Then you can use it the same way:
my_laptop = Laptop.new
my_laptop.brand_name = "HP"
puts my_laptop.brand_name # => "HP"
In line 45, you are calling the method set_brand_name=, but your Laptop class doesn't have a method with that name. You need to either call the method which you do have (set_brand_name), or rename the set_brand_name method to set_brand_name=.
Note that neither of those two is idiomatic, though. Idiomatically, the method should be named brand_name= (without the set_ prefix, the "setting" part is already implied by the = sign), and you shouldn't define it manually, but programmatically using the Module#attr_writer method.
Your entire code can be condensed to:
Laptop = Struct.new(:brand_name, :color, :processor, :storage, :memory)
my_laptop = Laptop.new('HP', 'Silver', 'i7-4700k', '750gb', '16gb')
brand = my_laptop.brand_name
color = my_laptop.color
processor = my_laptop.processor
memory = my_laptop.memory
storage = my_laptop.storage
puts "The Laptop I want is an #{brand}, it has a #{processor}, #{memory} of ram, a #{storage}, and it's #{color}!!!"
Your setter methods are defined incorrectly.
Here's your definition of the set_brand_name method:
def set_brand_name(brand_name)
#brand = brand_name
end
And here's how you're calling it:
my_laptop.set_brand_name = "HP"
You're calling the method incorrectly. If you'd like to keep your definition, you should be calling it like this:
my_laptop.set_brand_name("HP")
Or, if you'd like to use the equals sign, you should define your method like this:
def set_brand_name=(brand_name)
#brand = brand_name
end
Notice the equals in the method definition? You're required to use it when you want the setter to look like a regular assignment.
However, for most trivial cases you don't need to define getters and setters manually. You can just use attr_accessor on the class and pass it the properties you want to define. Here's what your class would look like with attr_accessor:
class Laptop
attr_accessor: :brand_name, :color, :processor, :storage, :memory
end
my_laptop = Laptop.new
my_laptop.brand_name = "HP"
my_laptop.processor = 'i7-4700k'
my_laptop.memory = '16gb'
my_laptop.storage = '750gb'
my_laptop.color = 'Silver'
brand = my_laptop.brand_name
color = my_laptop.color
processor = my_laptop.processor
memory = my_laptop.memory
storage = my_laptop.storage
puts """The Laptop I want is an #{brand}, it has a #{processor},
#{memory} of ram, a #{storage}, and it #{color}!!!"""
I encourage you to try it.

Initializing an array of instance variables

I'm making a game with a Board class Cell class. The Board class needs to be initialized with a unique instance variable for each Cell. I can hardcode it so that it works, but it seems inelegant and doesn't allow the size of the board to be chosen by the user at runtime. Here's what I have:
class Board
def initialize
#cell_1 = Cell.new(1)
#cell_2 = Cell.new(2)
#cell_3 = Cell.new(3)
#cell_4 = Cell.new(4)
#cell_5 = Cell.new(5)
#cell_6 = Cell.new(6)
#cell_7 = Cell.new(7)
#cell_8 = Cell.new(8)
#cell_9 = Cell.new(0)
#cells = [#cell_1, #cell_2, #cell_3,
#cell_4, #cell_5, #cell_6,
#cell_7, #cell_8, #cell_9]
end
end
I think I could use a loop to create a hash with unique key names pointing to unique Cell objects, but I don't know how I could make unique instance variables with a loop.
If you don't need to create each instance variables (#cell_1, #cell_2, ...), you can use Enumerable#map:
#cells = [*1..8, 0].map { |i| Cell.new(i) }
If you really need to refer every instance variable by name you can do something like this.
class Board
def initialize
#cells = (1..9).to_a.map { |i| Cell.new(i) }
end
def method_missing(method, *args, &block)
if method =~ /^cell_[1-9][0-9]*$/
index = method[/\d+/].to_i
#cells[index-1]
else
super
end
end
end
In this way you can call:
board = Board.new
board.cell_1 #=> first cell
Of course I'd use the solution proposed by #falsetru.

How can I create instances of a ruby class from a hash array?

I have a module FDParser that reads a csv file and returns a nice array of hashes that each look like this:
{
:name_of_investment => "Zenith Birla",
:type => "half-yearly interest",
:folio_no => "52357",
:principal_amount => "150000",
:date_of_commencement => "14/05/2010",
:period => "3 years",
:rate_of_interest => "11.25"
}
Now I have an Investment class that accepts the above hash as input and transforms each attribute according to what I need.
class Investment
attr_reader :name_of_investment, :type, :folio_no,
:principal_amount, :date_of_commencement,
:period, :rate_of_interest
def initialize(hash_data)
#name = hash_data[:name_of_investment]
#type = hash_data[:type]
#folio_no = hash_data[:folio_no]
#initial_deposit = hash_data[:principal_amount]
#started_on =hash_data[:date_of_commencement]
#term = hash_data[:period]
#rate_of_interest = hash_data[:rate_of_interest]
end
def type
#-- custom transformation here
end
end
I also have a Porfolio class with which I wish to manage a collection of investment objects. Here is what the Portfolio class looks like:
class Portfolio
include Enumerable
attr_reader :investments
def initialize(investments)
#investments = investments
end
def each &block
#investments.each do |investment|
if block_given?
block.call investment
else
yield investment
end
end
end
end
Now what I want is to loop over the investment_data yielded by the module and dynamically create instances of the investment class and then send those instances as input to the Portfolio class.
So far I tried:
FDParser.investment_data.each_with_index do |data, index|
"inv#{index+1}" = Investment.new(data)
end
But obviously this doesn't work because I get a string instead of an object instance. What is the right way to send a collection of instances to a enumerable collection class that can then manage them?
I'm not sure what "send as input to the Portfolio class" means; classes themselves don't accept "input". But if you're just trying to add Investment objects to the #investments instance variable inside an instance of Portfolio, try this:
portfolio = Portfolio.new([])
FDParser.investment_data.each do |data|
portfolio.investments << Investment.new(data)
end
Note that the array literal [] and the return value of portfolio.investments point to the self-same Array object here. This means you could equivalently do this, which arguably is a little clearer:
investments = []
FDParser.investment_data.each do |data|
investments << Investment.new(data)
end
Portfolio.new(investments)
And if you want to play a little code golf, it shrinks further if you use map.
investments = FDParser.investment_data.map {|data| Investment.new(data) }
Portfolio.new(investments)
I think this is a little harder to read than the previous option, though.

Resources