I am trying my hands at Ruby, below is the code that I am writing in 2 different ways to understand Ruby Classes. In first block I am using accessor method (combination of accessor read & write) and I want to print final line as "lord of the rings is written by Tolkien and has 400 pages". How can I make that happen? I understand that adding string and integer will throw an error. I can get them to print on separate lines, its just that I can't get them in a sentence.
class Book
attr_accessor :title, :author, :pages
end
book1 = Book.new()
book1.title = 'lord of the rings'
book1.author = 'Tolkien'
book1.pages = 400
puts book1.title
puts book1.author
puts book1.pages
#puts book1.title + " is written by " + book1.author + " and has " + book1.pages + " pages" <<<this errors out for known reason>>>
Second piece of code doing the same thing but I am using instance variable and have figured out how to get desired output. However, please advise if there's a better way of doing this?
class Novel
def initialize(title, author, pages)
#title = title
#author = author
#pages = pages
end
def inspect
"#{#title} is written by #{#author} and has #{#pages} pages"
end
end
novel1 = Novel.new('harry potter', 'JK Rowling', 300)
puts novel1.inspect
In your first example you are providing access the info you want and leaving it up to the client to format the output. For example you could have gotten what you wanted by adding this line in place of your commented line.
puts "#{book1.title} is written by #{book1.author} and has #{book1.pages} pages"
In your second example you are "pushing" that code down into the Novel class and proving a method to produce the output you want. BTW, don't use inspect as a method name, inspect is already a defined method
For example the following will print the same info twice.
class Novel
attr_accessor :title, :author, :pages
def initialize(title, author, pages)
#title = title
#author = author
#pages = pages
end
def info
"#{#title} is written by #{#author} and has #{#pages} pages"
end
end
novel = Novel.new('harry potter', 'JK Rowling', 300)
puts novel.info
puts "#{novel.title} is written by #{novel.author} and has #{novel.pages} pages"
Related
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.
Hi i have a question because i usually use multiple classes in Ruby but i dont know if is that correct?, for example the next code:
Class Main.rb
class Main
require_relative 'Archivo.rb'
require_relative 'Diccionario.rb'
require_relative 'Jsn.rb'
hsh = Diccionario.new
jsn = Jsn.new
fl = Archivo.new
puts "Ingresa Nombre"
nombre = gets
puts "Ingresa Correo"
correo = gets
puts "Ingresa password"
password = gets
hsh.usuario(nombre,correo,password)
jsn.convert_json(hsh.get_usuario)
fl.write('usuario.json',jsn.get_json)
fl.read('usuario.json')
puts fl.get_line
end
Class Diccionario.rb in other file
class Diccionario
$usuarios = Hash.new
require 'json'
def usuario(nombre, correo, password)
$usuarios = {nombre: nombre, correo: correo, password: password}
end
def get_usuario
$usuarios
end
end
Class Jsn.rb in other file
class Jsn
require 'json'
$cadena
def convert_json(cadena)
$cadena = cadena.to_json
end
def get_json
$cadena
end
end
Class Archivo.rb in other file
class Archivo
$line
def read(file)
File.open(file,"r") {|archivo|
$line =archivo.gets
}
end
def write(file,cadena)
File.open(file, "w+") do |f|
f.puts cadena
end
end
def get_line()
$line.to_s
end
end
Thanks :D
You should definitely be creating multiple classes! I would try to think in terms of SOLID principles to ascertain where behaviour belongs and which objects/classes have responsibility for implementing them.
There's an excellent book called Practical Object Oriented Design in Ruby by Sandi Metz. It's a great book about how to organise your code into classes. It talks about SRP (Single Responsibility Principle) and how to drill down to the essence of the problem you're trying to solve. If you can't buy the book then there's a talk that covers the main points here:
https://www.youtube.com/watch?v=8bZh5LMaSmE
She actually mentions that it's a common error to "not create enough classes" and that you should feel confident to break the implementation down further to smaller manageable classes.
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
I have a YAML file books.yaml:
- !ruby.object:Book
title: Ruby for Newbz
author: LeeRoy Jenkins
category: Educational
I already have a method that adds books to this file, but I need a method that can search the YAML file using a regular expression. If no book matches the title then it should raise an exception NoBookfound. If there are any matches, that list should be returned to the caller.
Here is my existing code:
require 'yaml'
require './book'
class Library
attr_accessor :books
def initialize file_name = false
#books = file_name ? YAML::load(File.read(file_name)) : []
end
def add_book(book)
#books.push(book)
end
def search_library(file_name , book)
if
YAML::load(File.read(file_name)).include?(book) == false
raise 'No Book Found'
end
end
end
This is something I tried for the exception portion, but I feel like I'm way off. I'm really new to Ruby and this seems to be a pretty hard task. Does anyone have any ideas?
What you need is to test the class from the loaded object:
book = YAML::load(File.read(file_name))
raise 'No Book Found' unless book.kind_of? Book
There are also some kind_of? alternatives, which you can read about the differences on this nice question
Based on your post, I guess that book param is a Regexp object.
I think you should remove the file_name param from search method, since the file is already loaded on initialize method.
My implemetation would be:
def search_library term
#books.find_all {|b| b.title =~ term}
end
Since you already have "#books" in an enumerable you can use the select method to find books whose title have the search term as a substring.
def search_library(term)
matches = #books.select { |x| x.title.index(term) }
raise "No books have title like '#{term}'" if matches.empty?
matches
end
This is a follow up question to: ruby variable scoping across classes. The solution makes sense to me conceptually, but I can't get it to work. Thought maybe with more code someone could help me.
I have a class Login that declares a new IMAP class, authenticates, and picks a mailbox.
I then am trying to create a separate class that will "do stuff" in the mailbox. For example, calculate the number of emails received. The problem is that the #imap instance of Net::IMAP doesn't pass from the Login class to the Stat class -- I'm getting no method errors for imap.search in the new class. I don't want to re-log in and re-authenticate each time I need to "do some stuff" with the mailbox. I'm trying to implement the solution in the other thread, but can't seem to get it to work.
Here's the Login class:
class Login
def initialize(user, domain, pass)
#username = user
#domain = domain
#pass = pass
#check if gmail or other domain
gmail_test = #domain.include? "gmail.com"
if gmail_test == true
#imap = Net::IMAP.new('imap.gmail.com',993,true,nil,false)
#imap.login(#username + "#" + #domain, #pass)
else
#imap = Net::IMAP.new("mail." + #domain)
#imap.authenticate('LOGIN', #username + "#" + #domain, #pass)
end
return self
end
#enable mailbox select
def mailbox(box)
#mailbox = box
#mailbox_array = #imap.list('','*').collect{ |mailbox| mailbox.name } #create array of mailboxes
#matching_mailbox = #mailbox_array.grep(/#{#mailbox}/i) #search for mailbox along list
if #matching_mailbox.empty? == true #if no results open INBOX
#mailbox = "INBOX"
#imap.examine(#mailbox)
else
#imap.examine(#matching_mailbox.first.to_s) #if multiple results, select first and examine
#mailbox = #matching_mailbox.first.to_s
end
return self
end
end
I want to be able to say:
Login.new("user", "domain", "pass").mailbox("in")
and then something like:
class Stat
def received_today()
#emails received today
#today = Date.today
#received_today = #imap.search(["SINCE", #today.strftime("%d-%b-%Y")]).count.to_s
puts #domain + " " + #mailbox.to_s + ": " + #received_today + " -- Today\n\n" #(" + #today.strftime("%d-%b-%Y") + ")
end
end
And be able to call
Stat.new.received_today and have it not throw a "no method search" error. Again, the other question contains pseudo_code and a high level explanation of how to use an accessor method to do this, but I can't implement it regardless of how many hours I've tried (been up all night)...
All I can think is that I am doing this wrong at a high level, and the stat calculation needs to be a method for the Login class, not a separate class. I really wanted to make it a separate class, however, so I could more easily compartmentalize...
Thanks!
Another approach that works and doesn't require defining get_var methods:
b.instance_variable_get("#imap") # where b = class instance of login
OK --- After much head banging on the wall, I got this to work.
Added these three methods to class Login:
def get_imap
#imap
end
def get_domain
#domain
end
def get_mailbox
#mailbox
end
Changed class Stat to:
class Stat
def received_today(login)
#emails received today
#today = Date.today
#received_today = login.get_imap.search(["SINCE", #today.strftime("%d-%b-%Y")]).count.to_s
# puts #received_today
puts login.get_domain + " " + login.get_mailbox.to_s + ": " + #received_today + " -- Today\n\n"
end
end
Now this actually works, and doesn't say undefined method search or imap:
b = Login.new("user", "domain", "pass").mailbox("box")
c = Stat.new
c.received_today(b)
I'm pretty sure there is a way to use attr_accessor to do this as well, but couldn't figure out the syntax. Anyway, this works and enables me to use the #imap var from class Login in class Stat so I can write methods to "do_stuff" with it. Thanks for the help and please don't hesitate to tell me this is horrible Ruby or not best practices. I'd love to hear the Ruby way to accomplish this.
Edit for attr_accessor or attr_reader use:
Just add it to class Login and then can say login.imap.search#stuff in class Stat with no problem.