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.
Related
I have recently started using Rubocop to "standardise" my code, and it has helped me optimise a lot of my code, as well as help me learn a lot of Ruby "tricks". I understand that I should use my own judgement and disable Cops where necessary, but I have found myself quite stuck with the below code:
def index
if params[:filters].present?
if params[:filters][:deleted].blank? || params[:filters][:deleted] == "false"
# if owned is true, then we don't need to filter by admin
params[:filters][:admin] = nil if params[:filters][:admin].present? && params[:filters][:owned] == "true"
# if admin is true, then must not filter by owned if false
params[:filters][:owned] = nil if params[:filters][:owned].present? && params[:filters][:admin] == "false"
companies_list =
case params[:filters][:admin]&.to_b
when true
current_user.admin_companies
when false
current_user.non_admin_companies
end
if params[:filters][:owned].present?
companies_list ||= current_user.companies
if params[:filters][:owned].to_b
companies_list = companies_list.where(owner: current_user)
else
companies_list = companies_list.where.not(owner: current_user)
end
end
else
# Filters for deleted companies
companies_list = {}
end
end
companies_list ||= current_user.companies
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
Among others, the error that I'm getting is the following:
C: Metrics/AbcSize: Assignment Branch Condition size for index is too high. [<13, 57, 16> 60.61/15]
I understand the maths behind it, but I don't know how to simplify this code to achieve the same result.
Could someone please give me some guidance on this?
Thanks in advance.
Well first and foremost, is this code fully tested, including all the myriad conditions? It's so complex that refactoring will surely be disastrous unless the test suite is rigorous. So, write a comprehensive test suite if you don't already have one. If there's already a test suite, make sure it tests all the conditions.
Second, apply the "fat model skinny controller" paradigm. So move all the complexity into a model, let's call it CompanyFilter
def index
companies_list = CompanyFilter.new(current_user, params).list
response = { data: companies_list.alphabetical.as_json(current_user: current_user) }
json_response(response)
end
and move all those if/then/else statements into the CompanyFilter#list method
tests still pass? great, you'll still get the Rubocop warnings, but related to the CompanyFilter class.
Now you need to untangle all the conditions. It's a bit hard for me to understand what's going on, but it looks as if it should be reducible to a single case statement, with 5 possible outcomes. So the CompanyFilter class might look something like this:
class CompanyFilter
attr_accessors :current_user, :params
def initialize(current_user, params)
#current_user = current_user
#params = params
end
def list
case
when no_filter_specified
{}
when user_is_admin
#current_user.admin_companies
when user_is_owned
# etc
when # other condition
# etc
end
end
private
def no_filter_specified
#params[:filter].blank?
end
def user_is_admin
# returns boolean based on params hash
end
def user_is_owned
# returns boolean based on params hash
end
end
tests still passing? perfect! [Edit] Now you can move most of your controller tests into a model test for the CompanyFilter class.
Finally I would define all the different companies_list queries as scopes on the Company model, e.g.
class Company < ApplicationRecord
# some examples, I don't know what's appropriate in this app
scope :for_user, ->(user){ where("...") }
scope :administered_by, ->(user){ where("...") }
end
When composing database scopes ActiveRecord::SpawnMethods#merge is your friend.
Post.where(title: 'How to use .merge')
.merge(Post.where(published: true))
While it doesn't look like much it lets you programatically compose scopes without overelying on mutating assignment and if/else trees. You can for example compose an array of conditions and merge them together into a single ActiveRecord::Relation object with Array#reduce:
[Post.where(title: 'foo'), Post.where(author: 'bar')].reduce(&:merge)
# => SELECT "posts".* FROM "posts" WHERE "posts"."title" = $1 AND "posts"."author" = $2 LIMIT $3
So lets combine that with a skinny controllers approach where you handle filtering in a seperate object:
class ApplicationFilter
include ActiveModel::Attributes
include ActiveModel::AttributeAssignment
attr_accessor :user
def initialize(**attributes)
super()
assign_attributes(attributes)
end
# A convenience method to both instanciate and apply the filters
def self.call(user, params, scope: model_class.all)
return scope unless params[:filters].present?
scope.merge(
new(
permit_params(params).merge(user: user)
).to_scope
)
end
def to_scope
filters.map { |filter| apply_filter(filter) }
.compact
.select {|f| f.respond_to?(:merge) }
.reduce(&:merge)
end
private
# calls a filter_by_foo method if present or
# defaults to where(key => value)
def apply_filter(attribute)
if respond_to? "filter_by_#{attribute}"
send("filter_by_#{attribute}")
else
self.class.model_class.where(
attribute => send(attribute)
)
end
end
# Convention over Configuration is sexy.
def self.model_class
name.chomp("Filter").constantize
end
# filters the incoming params hash based on the attributes of this filter class
def self.permit_params
params.permit(filters).reject{ |k,v| v.blank? }
end
# provided for modularity
def self.filters
attribute_names
end
end
This uses some of the goodness provided by Rails to setup objects with attributes that will dynamically handle filtering attributes. It looks at the list of attributes you have declared and then slices those off the params and applies a method for that filter if present.
We can then write a concrete implementation:
class CompanyFilter < ApplicationFilter
attribute :admin, :boolean, default: false
attribute :owned, :boolean
private
def filter_by_admin
if admin
user.admin_companies
else
user.non_admin_companies
end
end
# this should be refactored to use an assocation on User
def filter_by_owned
case owned
when nil
nil
when true
Company.where(owner: user)
when false
Company.where.not(owner: user)
end
end
end
And you can call it with:
# scope is optional
#companies = CompanyFilter.call(current_user, params), scope: current_user.companies)
I need to clone an existing object and change that cloned object.
The problem is that my changes change original object.
Here's the code:
require "httparty"
class Http
attr_accessor :options
attr_accessor :rescue_response
include HTTParty
def initialize(options)
options[:path] = '/' if options[:path].nil? == true
options[:verify] = false
self.options = options
self.rescue_response = {
:code => 500
}
end
def get
self.class.get(self.options[:path], self.options)
end
def post
self.class.post(self.options[:path], self.options)
end
def put
self.class.put(self.options[:path], self.options)
end
def delete
self.class.put(self.options[:path], self.options)
end
end
Scenario:
test = Http.new({})
test2 = test
test2.options[:path] = "www"
p test2
p test
Output:
#<Http:0x00007fbc958c5bc8 #options={:path=>"www", :verify=>false}, #rescue_response={:code=>500}>
#<Http:0x00007fbc958c5bc8 #options={:path=>"www", :verify=>false}, #rescue_response={:code=>500}>
Is there a way to fix this?
You don't even need to clone here, you just need to make a new instance.
Right here:
test = Http.new({})
test2 = test
you don't have two instances of Http, you have one. You just have two variables pointing to the same instance.
You could instead change it to this, and you wouldn't have the problem.
test = Http.new({})
test2 = Http.new({})
If, however, you used a shared options argument, that's where you'd encounter an issue:
options = { path: nil }
test = Http.new(options)
# options has been mutated, which may be undesirable
puts options[:path] # => "/"
To avoid this "side effect", you could change the initialize method to use a clone of the options:
def initialize(options)
options = options.clone
# ... do other stuff
end
You could also make use of the splat operator, which is a little more cryptic but possibly more idiomatic:
def initialize(**options)
# do stuff with options, no need to clone
end
You would then call the constructor like so:
options = { path: nil }
test = Http.new(**options)
puts test.options[:path] # => "/"
# the original hasn't been mutated
puts options[:path] # => nil
You want .clone or perhaps .dup
test2 = test.clone
But depending on your purposes, but in this case, you probably want .clone
see What's the difference between Ruby's dup and clone methods?
The main difference is that .clone also copies the objects singleton methods and frozen state.
On a side note, you can also change
options[:path] = '/' if options[:path].nil? # you don't need "== true"
I'm working on a program that receives responses from an API that represent 'songs' from a database. Those responses arrive in my program as Struct objects, and they are structured slightly differently depending on which table they were pulled from.
For instance, the song object pulled from the 'track' table looks like:
song_1 = <struct Song track_artist="Michael Jackson", track_title="Billie Jean">
And the song object returned from the 'license' table looks like:
song_2 = <struct Song license_artist="Michael Jackson", license_title="Billie Jean">
If I want to get the 'artist' from song_1, I'd call song_1.track_artist, and with song_2, song_2.license_artist. But this is problematic when running loops. I want to be able to call song.title on any of them and receive the title.
Right now, I'm putting each Struct through a 'Normalizer' object when I receive it. It uses a hash mapping to change the method name of each Struct; the mapping more or less looks like:
{ track_artist: artist,
track_title: title,
license_artist: artist,
license_title: title }
This seems like it might be overkill. What's the best way to go about this?
You could use method_missing for this
module Unifier
def method_missing(name, *args, &block)
meth = public_methods.find { |m| m[/_#{name}/] }
meth ? send(meth, *args, *block) : super
end
def respond_to_missing?(method_name, include_private = false)
public_methods.any? { |m| m[/_#{method_name}/] } || super
end
end
class A
include Unifier
attr_reader :artist_name
def initialize
#artist_name = 123
end
end
a = A.new
a.respond_to?(:name) # => true
a.name # => 123
a.respond_to?(:title) # => false
a.title # => undefined method `title' for #<A:0x007fb3f4054330 #artist_name=123> (NoMethodError)
Update
For you case it will be more complex and tricky.
If you can make changes to place, where this Struct objects are created, then just patch classes, generated from Struct
song_1_class = Struct.new(:track_artist, :track_title) do
include Unifier
end
song_1 = song_1_class.new('Michael Jackson', 'Billie Jean')
puts "#{song_1.artist} - #{song_1.title}"
# => Michael Jackson - Billie Jean
If you can work only with objects of that classes - you could patch it dynamically
# We get objects of licence_struct class
licence_struct = Struct.new(:license_artist, :license_title)
song_2 = licence_struct.new('Michael Jackson', 'Billie Jean')
song_3 = licence_struct.new('Michael Jackson', 'Black of White')
def process_song(song)
puts "Song #{song} patched - #{song.respond_to?(:artist)}"
"#{song.artist} - #{song.title}"
rescue NoMethodError => err
# If we don't have methods on our struct - patch it
# If after patching object still dont respond to our method - throw exception
patch_object_from_error(err) ? retry : raise(err)
end
def patch_object_from_error(error)
receiver = error.receiver
receiver.class.class_exec { include Unifier }
meth = error.message.match(/undefined method `(\S+)'/)[1].to_sym
receiver.respond_to?(meth)
end
puts process_song(song_2)
# => Song #<struct license_artist="Michael Jackson", license_title="Billie Jean"> patched - false
# after retry
# => Song #<struct license_artist="Michael Jackson", license_title="Billie Jean"> patched - true
# => Michael Jackson - Billie Jean
puts process_song(song_3)
# dont need retry - class already patched
# => Song #<struct license_artist="Michael Jackson", license_title="Black of White"> patched - true
# => Michael Jackson - Black of White
I'm new to Ruby and am working on an exercise where a sports team (of up to 10 ppl) have to have at least 2 men and 2 women. I've created a Player class where the player's gender is determined and a Team class, where I add these players into an instance variable array of #team (which is created upon initialization of Team).
I have placed my full code towards the bottom of this request.
I'm hoping someone can please help me on the following:
(1) What do I type in IRB to be able to specifically recall/maniuplate the #team instance variable array (which stores all the players). I wish to later iterate over the array or extract the first item in the array (#team.first does not work)
(2) I'm having difficulty writing the code to determine if at least 2 male and female players are in the #team instance variable. The best code I came up in the Team class was the following - but it is reporting that #team is nilclass.
def gender_balance
#team.select{|player| player.male == true }
end
I have researched the internet and tried various combinations for an answer - with no success.
Directly below are the IRB commands that I have typed to create and add players to teams. Later below is my code for Team (which contains the method to assess whether it has the right gender mix) and Players.
irb(main):001:0> team = Team.new
=> #<Team:0x007fd8f21df858 #team=[]>
irb(main):002:0> p1 = Player.new
=> #<Player:0x007fd8f21d7b08 #male=true>
irb(main):003:0> p1.female_player
=> false
irb(main):004:0> p2 = Player.new
=> #<Player:0x007fd8f21bff58 #male=true>
irb(main):005:0> team.add_player(p1)
=> [#<Player:0x007fd8f21d7b08 #male=false>]
irb(main):006:0> team.add_player(p2)
=> [#<Player:0x007fd8f21d7b08 #male=false>, #<Player:0x007fd8f21bff58 #male=true>]
These two IRB lines are me trying unsucessfully to recall the contents of #team
irb(main):007:0> team
=> #<Team:0x007fd8f21df858 #team=[#<Player:0x007fd8f21d7b08 #male=false>, #<Player:0x007fd8f21bff58 #male=true>]>
irb(main):013:0> #team
=> nil
The code for both classes is below:
class Player
def initialize
#male = true
end
def female_player
#male = false
end
def male_player
#male
end
end
class Team
def initialize
#team = []
end
def add_player player
#team << player
end
def player_count
#team.count
end
def valid_team?
player_number_check
gender_balance
end
private
def player_number_check
player_count > 6 && player_count < 11
end
def gender_balance
#team.select{|player| player.male == true }
end
end
My github reference for this code is: https://github.com/elinnet/object-calisthenics-beach-volleyball-edition.git
Thank you.
Your Team class does not have an attribute for getting the #team instance variable.† So the only way you can extract its value is by using instance_variable_get:
irb(main):029:0> team = Team.new
=> #<Team:0x007fff4323fd58 #team=[]>
irb(main):030:0> team.instance_variable_get(:#team)
=> []
Please don't use instance_variable_get for actual production code though; it's a code smell. But for the purposes of inspecting instance variables in IRB, it's okay.
† You'd normally define one using either attr_accessor :team (read/write) or attr_reader :team (read-only) inside the class definition.
I'm writing something that is a bit like Facebook's shared link preview.
I would like to make it easily extendable for new sites by just dropping in a new file for each new site I want to write a custom parser for. I have the basic idea of the design pattern figured out but don't have enough experience with modules to nail the details. I'm sure there are plenty of examples of something like this in other projects.
The result should be something like this:
> require 'link'
=> true
> Link.new('http://youtube.com/foo').preview
=> {:title => 'Xxx', :description => 'Yyy', :embed => '<zzz/>' }
> Link.new('http://stackoverflow.com/bar').preview
=> {:title => 'Xyz', :description => 'Zyx' }
And the code would be something like this:
#parsers/youtube.rb
module YoutubeParser
url_match /(youtube\.com)|(youtu.be)\//
def preview
get_stuff_using youtube_api
end
end
#parsers/stackoverflow.rb
module SOFParser
url_match /stachoverflow.com\//
def preview
get_stuff
end
end
#link.rb
class Link
def initialize(url)
extend self with the module that has matching regexp
end
end
# url_processor.rb
class UrlProcessor
# registers url handler for given pattern
def self.register_url pattern, &block
#patterns ||= {}
#patterns[pattern] = block
end
def self.process_url url
_, handler = #patterns.find{|p, _| url =~ p}
if handler
handler.call(url)
else
{}
end
end
end
# plugins/so_plugin.rb
class SOPlugin
UrlProcessor.register_url /stackoverflow\.com/ do |url|
{:title => 'foo', :description => 'bar'}
end
end
# plugins/youtube_plugin.rb
class YoutubePlugin
UrlProcessor.register_url /youtube\.com/ do |url|
{:title => 'baz', :description => 'boo'}
end
end
p UrlProcessor.process_url 'http://www.stackoverflow.com/1234'
#=>{:title=>"foo", :description=>"bar"}
p UrlProcessor.process_url 'http://www.youtube.com/1234'
#=>{:title=>"baz", :description=>"boo"}
p UrlProcessor.process_url 'http://www.foobar.com/1234'
#=>{}
You just need to require every .rb from plugins directory.
If you're willing to take this approach you should probably scan the filed for the mathing string and then include the right one.
In the same situation I attempted a different approach. I'm extending the module with new methods, ##registering them so that I won't register two identically named methods. So far it works good, though the project I started is nowhere near leaving the specific domain of one tangled mess of a particular web-site.
This is the main file.
module Onigiri
extend self
##registry ||= {}
class OnigiriHandlerTaken < StandardError
def description
"There was an attempt to override registered handler. This usually indicates a bug in Onigiri."
end
end
def clean(data, *params)
dupe = Onigiri::Document.parse data
params.flatten.each do |method|
dupe = dupe.send(method) if ##registry[method]
end
dupe.to_html
end
class Document < Nokogiri::HTML::DocumentFragment
end
private
def register_handler(name)
unless ##registry[name]
##registry[name] = true
else
raise OnigiriHandlerTaken
end
end
end
And here's the extending file.
# encoding: utf-8
module Onigiri
register_handler :fix_backslash
class Document
def fix_backslash
dupe = dup
attrset = ['src', 'longdesc', 'href', 'action']
dupe.css("[#{attrset.join('], [')}]").each do |target|
attrset.each do |attr|
target[attr] = target[attr].gsub("\\", "/") if target[attr]
end
end
dupe
end
end
end
Another way I see is to use a set of different (but behaviorally indistinguishable) classes with a simple decision making mechanism to call a right one. A simple hash that holds class names and corresponding url_matcher would probably suffice.
Hope this helps.