Ruby - Passing instance variables to a class from another - ruby

I have 3 classes: Invoice, Address and Customer (but for this problem, only the Invoice and Address class are relevant)
This is my Invoice class:
class Invoice
attr_reader :billing_address, :shipping_address, :order
def initialize(attributes = {})
#billing_address = attributes.values_at(:billing_address)
#shipping_address = attributes.values_at(:shipping_address)
#order = attributes.values_at(:order)
end
end
and this is my Address class:
class Address
attr_reader :zipcode, :full_address
def initialize(zipcode:)
#zipcode = zipcode
url = 'https://viacep.com.br/ws/' + zipcode.to_s + '/json/'
uri = URI(url)
status = Net::HTTP.get_response(uri)
if (status.code == "200")
response = Net::HTTP.get(uri)
full_address = JSON.parse(response)
#full_address = full_address
else
p "Houve um erro. API indisponível. Favor tentar novamente mais tarde."
#full_adress = nil
end
end
end
And this is my Customer class (not much relevant, but i'm showing for better explanation of the problem)
class Customer
attr_reader :name, :age, :email, :gender
def initialize(attributes = {})
#name = attributes.values_at(:name)
#age = attributes.values_at(:age)
#email = attributes.values_at(:email)
#gender = attributes.values_at(:gender)
end
end
As you can see, my Invoice class has 3 instance variables and my Address class has 2 instance variables.
So, if i test something like that:
cliente = Customer.new(name: "Lucas", age: 28, email: "abc#gmail.com", gender: "masculino")
endereco = Address.new(zipcode: 41701035)
entrega = Invoice.new(billing_address: endereco, shipping_address: endereco)
p endereco.instance_variables
[:#zipcode, :#full_address]
p entrega.shipping_address.instance_variables
[]
My instance variables can be acessed through the variable "endereco", that is an Address object, but can't be acessed through entrega.shipping_address that is also an Address object.
To be more precise, if a try this:
p entrega.shipping_address
I get this return:
[#<Address:0x00000001323d58 #zipcode=41701035, #full_address={"cep"=>"41701-035", "logradouro"=>"Rua Parati", "complemento"=>"", "bairro"=>"Alphaville I", "localidade"=>"Salvador", "uf"=>"BA", "unidade"=>"", "ibge"=>"2927408", "gia"=>""}>]
My full object are being returned, but i can't access the content of my #full_address instance variable.
If a do this:
p entrega.shipping_address.full_address
I get a NoMethodError:
solucao.rb:8:in `<main>': undefined method `full_address' for #<Array:0x000000012d25e8> (NoMethodError)
I'm trying to understand why i can't access the content inside my object if i have the full object. Maybe i'm trying to access in the wrong way, i don't know.
Can someone help ?

values_at returns an array of values (see https://apidock.com/ruby/Hash/values_at
for explanation)
Change
#shipping_address = attributes.values_at(:shipping_address)
into
#shipping_address = attributes[:shipping_address]
And that way #shipping_address will contain an Address object, not an array that contains an Address object

If you take a look at the error, it says
undefined method `full_address' for #<Array:0x000000012d25e8>
You're trying to call full_address on an array. So this means that entrega.shipping_address returns you an array (which it does, of course, take a closer look at the output).
If I were you, I'd look into how shipping_address is implemented. It's a simple attr_reader, so it's backed by an instance variable. Must be you initialize that instance variable to a wrong value (it gets an array instead of an address). Look closely at that initialization code and try to run it in IRB session. You should see the problem.

Related

Ruby iterating in a class method while referencing another class

Solved. I didn't have the initialization right, which gave the noMethodError. Then I was changing an array, but checking a variable that referred to a position in the array, and that variable had not been reassigned.
Edited to initialize bookPagesInfo, bookChaptersInfo, editPagesInfo, and editChapterInfo as suggested. Still gives the same NoMethod Error.
I have a book with page and chapter info, and want to be able to apply edits that change the number of pages, introPages, chapters, and povs.
class Book
attr_accessor :pages, :chapters, :bookPagesInfo, :bookChaptersInfo, :introPages, :povs
def initialize(bookPagesInfo, bookChaptersInfo)
#bookPagesInfo = bookPagesInfo
#bookChaptersInfo = bookChaptersInfo
#pages = bookPagesInfo[0]
#introPages = bookPagesInfo[1]
#chapters = bookChaptersInfo[0]
#povs = bookChaptersInfo[1]
end
def applyEdit(edit)
#pages += edit.new_pages
end
end
class Edit
attr_accessor :new_pages, :new_chapters, :editPagesInfo, :editChaptersInfo, :new_intro_pages, :new_povs
def initialize(editPagesInfo, editChaptersInfo)
#editPagesInfo = editPagesInfo
#editChaptersInfo = editChaptersInfo
#new_pages = editPagesInfo[0]
#new_intro_pages = editPagesInfo[1]
#new_chapters = editChaptersInfo[0]
#new_povs = editChaptersInfo[1]
end
end
The above code works for editing just number of pages. However, if I change my applyEdit method to iterate over the bookPagesInfo array, I can't get it to work. Running applyEdit below gives a nonfatal error.
def applyEdit(edit)
#bookPagesInfo.each_with_index do {|stat, idx| stat += edit.bookPagesInfo[idx]}
end
## gives undefined method `each_with_index' for nil:NilClass (NoMethodError), but
## my understanding is as long as bookPagesInfo was initialized as an array, it
## should be an array, not nilClass
I'm pretty new to classes (and this website, sorry for formatting). Thanks for the help.
You've got attr_accessors defined for :bookPagesInfo and :bookChaptersInfo, which will give you reader and writer methods, but it won't set #bookPagesInfo and #bookChaptersInfo for you in the initialize method - you need to do that yourself. So, when you try to read from the instance variable in applyEdit, you're reading nil.
Try adding
#bookPagesInfo = bookPagesInfo
#bookChaptersInfo = bookChaptersInfo
in Book#initialize.

Isolate dependencies while building object relationships in ruby

I'm practicing OOP for the first time by modeling my application domain (public high school) as objects, and I'm stuck on how to create relationships between the classes without introducing lots of external dependencies.
I have lots of relationships I want to construct, so in hopes of learning the general principle I'm giving two classes and sample objects here to illustrate the difficulty I'm having.
I have two classes Gradeand Transcript. Every instance of Transcript has an instance variable #mark, which right now is a string. I collected all the instances of each class a grades hash and a transcripts hash.
Question: How can I modify these classes so that #mark references the corresponding Grade instance?
(or, is that the wrong approach entirely?)
Grade has an instance for every possible final grade students can receive
class Grade
attr_accessor :mark, :alpha_equivalent, :numeric_range_low, :numeric_range_high, :numeric_qquivalent, :pass_fail_equivalent, :description
def initialize(args)
#mark = args["Mark"]
#alpha_equivalent = args["AlphaEquivalent"]
#numeric_range_low = args["NumericRangeLow"]
#numeric_range_high = args["NumericRangeHigh"]
#numeric_equivalent = args["NumericEquivalent"]
#pass_fail_equivalent = args["PassFailEquivalent"]
#description = args["Description"]
end
end
Sample object from the grades hash:
grades["100"] =>
#<Grade:0x007f9fcb077d68
#alpha_equivalent="100",
#description="100 out of 100",
#mark="100",
#numeric_equivalent="100",
#numeric_range_high="100",
#numeric_range_low="100",
#pass_fail_equivalent="P">
Transcript has instances for every final grade the student has ever received for all the courses they've studied
class Transcript
attr_accessor :student_id, :last_name, :first_name, :grade, :official_class, :school, :year, :term, :course, :course_title, :mark, :pass_fail, :credits
def initialize(args)
#student_id = args["StudentID"]
#last_name = args["LastName"]
#first_name = args["FirstName"]
#grade = args["Grade"]
#official_class = args["OffClass"]
#school = args["school"]
#year = args["Year"]
#term = args["Term"]
#course = args["Course"]
#course_title = args["Course Title"]
#mark = args["Mark"]
#credits = args["Credits"]
#grade_entry_cohort = args["GEC"]
end
end
Sample object from the transcripts hash:
transcripts["foobar-COURSE1-100"] =>
#<Transcript:0x007f9fce8786b8
#course="COURSE1",
#course_title="Example Course",
#credits="5",
#first_name="FOO",
#grade="100",
#grade_entry_cohort="V",
#last_name="BAR",
#mark="100",
#official_class="000",
#school="1",
#student_id="0123",
#term="1",
#year="2000">
I'm instantiating all the objects from CSV source files and then collecting them into a hash because I wanted to be able to address them directly.
Sounds like you need to want Transcript#grade to return a Grade instance. So let's make a method for that:
class Grade
def self.all
#all ||= {}
end
def self.find(mark)
all[mark]
end
end
Now, Grade.all needs to be populated. This could be achieved like this from your CSV:
grade_args = %w[alpha_equivalent description mark numeric_equivalent numeric_range_high numeric_range_low pass_fail_equivalent]
CSV.parse { |row| Grade.all.merge(csv['mark'] => Grade.new(row.slice(*grade_args)}
Now, we can modify Transcript like this:
class Transcript
def initialize(args)
#args = args
end
def grade
#grade ||= Grade.find(args['mark'])
end
private
attr_reader :args
end
Assuming that you've created the grades hash earlier:
# read args from csv
# id built from first, last, course, and grade
transcripts[id] = Transcript.
new(args.merge('Mark' => grades[args['Mark']])
It uses Hash#merge to extend args with an instance of Grade that was built earlier.

Assert_equal undefined local variable LRTHW ex52

Hi I made it to the lase exercise os Learn Ruby The Hard Way, and I come at the wall...
Here is the test code:
def test_gothon_map()
assert_equal(START.go('shoot!'), generic_death)
assert_equal(START.go('dodge!'), generic_death)
room = START.go("tell a joke")
assert_equal(room, laser_weapon_armory)
end
And here is the code of the file it should test:
class Room
attr_accessor :name, :description, :paths
def initialize(name, description)
#name = name
#description = description
#paths = {}
end
def ==(other)
self.name==other.name&&self.description==other.description&&self.paths==other.paths
end
def go(direction)
#paths[direction]
end
def add_paths(paths)
#paths.update(paths)
end
end
generic_death = Room.new("death", "You died.")
And when I try to launch the test file I get an error:
generic_death = Room.new("death", "You died.")
I tried to set the "generic_death = Room.new("death", "You died.")" in test_gothon_map method and it worked but the problem is that description of the next object is extremely long, so my questions are:
why assertion doesn't not respond to defined object?
can it be done different way then by putting whole object to testing method, since description of the next object is extremely long...
The nature of local variable is that they are, well, local. This means that they are not available outside the scope they were defined.
That's why ruby does not know what generic_death means in your test.
You can solve this in a couple of ways:
define rooms as constants in the Room class:
class Room
# ...
GENERIC_DEATH = Room.new("death", "You died.")
LASER_WEAPON_ARMORY = Room.new(...)
end
def test_gothon_map()
assert_equal(Room::START.go('shoot!'), Room::GENERIC_DEATH)
assert_equal(Room::START.go('dodge!'), Room::GENERIC_DEATH)
room = Room::START.go("tell a joke")
assert_equal(room, Room::LASER_WEAPON_ARMORY)
end
assert the room by its name, or some other identifier:
def test_gothon_map()
assert_equal(START.go('shoot!').name, "death")
assert_equal(START.go('dodge!').name, "death")
room = START.go("tell a joke")
assert_equal(room.name, "laser weapon armory")
end

ruby, no method error

I am receiving the following error when running my below ruby script:
s3parse.rb:12:in `block in <class:AccountLog>': undefined method `extract_account_id' for AccountLog:Class (NoMethodError)
I dont think it should be a class method, is there a reason its not taking my method into account?
class AccountLog
attr_accessor :bytes, :account_id, :date
def extract_account_id(line)
line.match(%r{accounts/(\d+)}).captures.join.to_i
end
s3log = File.open('vidcoder.txt').each do |line|
account_log = AccountLog.new
account_log.date = line.match(%r{\[[^:]*}).to_s.delete"[" #need to finish this regex to make it work
account_log.account_id = extract_account_id(line)
account_log.bytes = line.match(%r{^.*\s+HTTP.*\s+-\s+(\d+)\s+}).captures.join.to_i
puts "\n"
puts "The api request on #{account_log.date} was fromm account number #{account_log.account_id} and the bytes were #{account_log.bytes}"
end
end
def extract_account_id will define an instance method.
In the way you call it, you need a class method instead.
Define it like this:
def self.extract_account_id(line)
or, as you already have an AccountLog instance, use it to call extract_account_id:
account_log.account_id = account_log.extract_account_id(line)
Please note that with second way you do not need to alter method definition, just call extract_account_id via account_log instance.
And i guess you would want to put s3log = File... outside class definition.
Or use a constant instead: S3log = ...
Then you'll can access it as AccountLog::S3log
Is there any reason you don't think it should be a class method? You are using it in the context of a class method and that's why it it's saying no such method for class AccountLog.
If you name your method as self.extract_account_id(line) I'm sure it will work.
From what you are trying to do I think this is what you are looking for?
class AccountLog
attr_accessor :bytes, :account_id, :date
def self.extract_account_id(line)
line.match(%r{accounts/(\d+)}).captures.join.to_i
end
end
s3log = File.open('vidcoder.txt').each do |line|
account_log = AccountLog.new
account_log.date = line.match(%r{\[[^:]*}).to_s.delete"[" #need to finish this regex to make it work
account_log.account_id = extract_account_id(line)
account_log.bytes = line.match(%r{^.*\s+HTTP.*\s+-\s+(\d+)\s+}).captures.join.to_i
puts "\n"
puts "The api request on #{account_log.date} was fromm account number #{account_log.account_id} and the bytes were #{account_log.bytes}"
end
While you could take the class method approach, there seems to be a little more going on.
You should put the extraction logic in a method in itself rather than let it hangout in your class. Then outside of the class, have an instance of AccountLog where you can call on the methods for log and account id extraction. At that point you can do something with those values.
Class method or not is a detail we can explore after the class is a bit more clean I think.

Ruby - Method call to object in array

I'm working with a Ruby project for school, and have sadly not been able to find an answer to this question in my literature.
I have an array of camping lots, each containing a guest. I initialize the lots like this:
lots = Array.new
for i in (1..36)
lots[i] = Lot.new(i)
end
Further down I create a Guest object, initialize it, and now I want to add the Guest to my Lot. The method in the class Lot looks like this:
def AddGuest(guest)
#guest = guest
end
The problem comes when I want to call the method, as the Lot is in an Array.
lots[lotnumber].AddGuest(guest)
This call gives me the error:
undefined method `+#' for #<Guest:0x2c1ff14> (NoMethodError)
I have used require, so the classes know about each other. I've had quite a hard time understanding Ruby, could my error be that I try to access the AddGuest method in the Array class? I'm used to doing things like this in C++.
Below is the full source (the relevant parts at least).
Entire Lot class:
class Lot
def initialize(number)
#gauge = rand(2000) + 2000
#number = number
#guest = false
end
def Occupied()
return #guest
end
def AddGuest(guest)
#guest = guest
end
def RemoveGuest()
#guest = false
end
end
Parts of main.rb
#includes
require 'guest'
require 'lot'
#initiate comparison variables
userInput = "0"
numberOfGuests = 0
foundLot = false
guests = Array.new
lots = Array.new
#initialize lot list
for i in (1..36)
lots[i] = Lot.new(i)
end
Player input omitted
#make sure lot is not taken
while foundLot == false do
lotnumber = rand(35)+1
if lots[lotnumber].Occupied() == false then
foundLot = "true"
end
end
foundLot = false
guest = Guest.new(firstName, lastName, adress, phone, arrival, lotnumber)
guests.insert(numberOfGuests, guest)
numberOfGuests++
lots[lotnumber].AddGuest(guest) #this is where error hits
end
end
end
The error appears to be related to your use of the ++ operator, which is, quite naturally, supported in C++, but is not supported in Ruby.
The equivalent is:
numberOfGuests += 1
A couple little tips...
[1]
A slightly more idiomatic way to write this...
for i in (1..36)
lots[i] = Lot.new(i)
end
would be...
(1..36).each { |i| lots[i] << Lot.new(i) }
[2]
To remove a Guest from a Lot, you might want to set it to nil rather than false. This would be my suggestion...
class Lot
def initialize(number)
#gauge = rand(2000) + 2000
#number = number
# Don't need to set #guest -- it's nil by default.
end
# In Ruby, methods that return a boolean often have a "?".
# Makes it "read better" when you call the method. (See
# usage sample.)
def occupied?
! #guest.nil?
end
# There's a more commonplace way to do this. See below...
def add_guest(guest)
#guest = guest
end
def remove_guest()
#guest = nil
end
end
Example of usage:
>> lot = Lot.new(2)
=> #<Lot:0x1300920 #number=2, #gauge=3444>
>> lot.occupied
=> false
>> lot.add_guest('A guest')
=> "A guest"
>> lot.occupied?
=> true
>> lot.remove_guest
=> nil
>> lot.occupied?
=> false
Take two...
It's conventional to use attr_accessor methods in your class definition. They automatically add getter and setter methods to your class. You could do that instead of add_guest and remove_guest if you wanted to follow the common Ruby pattern...
class Lot
attr_accessor :number, :gauge, :guest
def initialize(number)
#gauge = rand(2000) + 2000
#number = number
end
def occupied?
! #guest.nil?
end
end
Usage...
irb(main):017:0> lot = Lot.new(3)
=> #<Lot:0xb7f7fca8 #gauge=3186, #number=3>
Set the Guest of a Lot (like add_guest)...
irb(main):019:0> lot.guest = 'A guest'
=> "A guest"
irb(main):020:0> lot.occupied?
=> true
Get the Guest for a Lot...
irb(main):025:0> lot.guest
=> "A guest"
Remove the Guest...
irb(main):021:0> lot.guest = nil
=> nil
irb(main):023:0> lot.occupied?
=> false
Generally Ruby method names are not capitalized. The convention are simply: ClassName, CONSTANT, method_name.
Since you have an Array of Lot objects, the following should be true:
lots.class # => Array
lots[1].class # => Lot
The method called should be defined for Lot.

Resources