Ruby Methods and Optional parameters - ruby-on-rails

I'm playing with Ruby on Rails and I'm trying to create a method with optional parameters. Apparently there are many ways to do it. I trying naming the optional parameters as hashes, and without defining them. The output is different. Take a look:
# This functions works fine!
def my_info(name, options = {})
age = options[:age] || 27
weight = options[:weight] || 160
city = options[:city] || "New York"
puts "My name is #{name}, my age is #{age}, my weight is #{weight} and I live in {city}"
end
my_info "Bill"
-> My name is Bill, my age is 27, my weight is 160 and I live in New York
-> OK!
my_info "Bill", age: 28
-> My name is Bill, my age is 28, my weight is 160 and I live in New York
-> OK!
my_info "Bill", weight: 200
-> My name is Bill, my age is 27, my weight is 200 and I live in New York
-> OK!
my_info "Bill", city: "Scottsdale"
-> My name is Bill, my age is 27, my weight is 160 and I live in Scottsdale
-> OK!
my_info "Bill", age: 99, weight: 300, city: "Sao Paulo"
-> My name is Bill, my age is 99, my weight is 300 and I live in Sao Paulo
-> OK!
****************************
# This functions doesn't work when I don't pass all the parameters
def my_info2(name, options = {age: 27, weight: 160, city: "New York"})
age = options[:age]
weight = options[:weight]
city = options[:city]
puts "My name is #{name}, my age is #{age}, my weight is #{weight} and I live in #{city}"
end
my_info2 "Bill"
-> My name is Bill, my age is 27, my weight is 160 and I live in New York
-> OK!
my_info2 "Bill", age: 28
-> My name is Bill, my age is 28, my weight is and I live in
-> NOT OK! Where is my weight and the city??
my_info2 "Bill", weight: 200
-> My name is Bill, my age is , my weight is 200 and I live in
-> NOT OK! Where is my age and the city??
my_info2 "Bill", city: "Scottsdale"
-> My name is Bill, my age is , my weight is and I live in Scottsdale
-> NOT OK! Where is my age and my weight?
my_info2 "Bill", age: 99, weight: 300, city: "Sao Paulo"
-> My name is Bill, my age is 99, my weight is 300 and I live in Sao Paulo
-> OK!
What's wrong with the second approach for optional parameters?
The second method only works if I don't pass any optional parameter or if I pass them all.
What am I missing?

The way optional arguments work in ruby is that you specify an equal sign, and if no argument is passed then what you specified is used. So, if no second argument is passed in the second example, then
{age: 27, weight: 160, city: "New York"}
is used. If you do use the hash syntax after the first argument, then that exact hash is passed.
The best you can do is
def my_info2(name, options = {})
options = {age: 27, weight: 160, city: "New York"}.merge(options)
...

The problem is the default value of options is the entire Hash in the second version you posted. So, the default value, the entire Hash, gets overridden. That's why passing nothing works, because this activates the default value which is the Hash and entering all of them also works, because it is overwriting the default value with a Hash of identical keys.
I highly suggest using an Array to capture all additional objects that are at the end of your method call.
def my_info(name, *args)
options = args.extract_options!
age = options[:age] || 27
end
I learned this trick from reading through the source for Rails. However, note that this only works if you include ActiveSupport. Or, if you don't want the overhead of the entire ActiveSupport gem, just use the two methods added to Hash and Array that make this possible.
rails / activesupport / lib / active_support / core_ext / array /
extract_options.rb
So when you call your method, call it much like you would any other Rails helper method with additional options.
my_info "Ned Stark", "Winter is coming", :city => "Winterfell"

If you want to default the values in your options hash, you want to merge the defaults in your function. If you put the defaults in the default parameter itself, it'll be over-written:
def my_info(name, options = {})
options.reverse_merge!(age: 27, weight: 160, city: "New York")
...
end

In second approach, when you say,
my_info2 "Bill", age: 28
It will pass {age: 28}, and entire original default hash {age: 27, weight: 160, city: "New York"} will be overridden. That's why it does not show properly.

You can also define method signatures with keyword arguments (New since, Ruby 2.0, since this question is old):
def my_info2(name, age: 27, weight: 160, city: "New York", **rest_of_options)
p [name, age, weight, city, rest_of_options]
end
my_info2('Joe Lightweight', weight: 120, age: 24, favorite_food: 'crackers')
This allows for the following:
Optional parameters (:weight and :age)
Default values
Arbitrary order of parameters
Extra values collected in a hash using double splat (:favorite_food collected in rest_of_options)

For the default values in your hash you should use this
def some_method(required_1, required_2, options={})
defaults = {
:option_1 => "option 1 default",
:option_2 => "option 2 default",
:option_3 => "option 3 default",
:option_4 => "option 4 default"
}
options = defaults.merge(options)
# Do something awesome!
end

To answer the question of "why?": the way you're calling your function,
my_info "Bill", age: 99, weight: 300, city: "Sao Paulo"
is actually doing
my_info "Bill", {:age => 99, :weight => 300, :city => "Sao Paulo"}
Notice you are passing two parameters, "Bill" and a hash object, which will cause the default hash value you've provided in my_info2 to be completely ignored.
You should use the default value approach that the other answerers have mentioned.

#fetch is your friend!
class Example
attr_reader :age
def my_info(name, options = {})
#age = options.fetch(:age, 27)
self
end
end
person = Example.new.my_info("Fred")
puts person.age #27

I don't see anything wrong with using an or operator to set defaults. Here's a real life example (note, uses rails' image_tag method):
file:
def gravatar_for(user, options = {} )
height = options[:height] || 90
width = options[:width] || 90
alt = options[:alt] || user.name + "'s gravatar"
gravatar_address = 'http://1.gravatar.com/avatar/'
clean_email = user.email.strip.downcase
hash = Digest::MD5.hexdigest(clean_email)
image_tag gravatar_address + hash, height: height, width: width, alt: alt
end
console:
2.0.0-p247 :049 > gravatar_for(user)
=> "<img alt=\"jim's gravatar\" height=\"90\" src=\"http://1.gravatar.com/avatar/<hash>\" width=\"90\" />"
2.0.0-p247 :049 > gravatar_for(user, height: 123456, width: 654321)
=> "<img alt=\"jim's gravatar\" height=\"123456\" src=\"http://1.gravatar.com/avatar/<hash>\" width=\"654321\" />"
2.0.0-p247 :049 > gravatar_for(user, height: 123456, width: 654321, alt: %[dogs, cats, mice])
=> "<img alt=\"dogs cats mice\" height=\"123456\" src=\"http://1.gravatar.com/avatar/<hash>\" width=\"654321\" />"
It feels similar to using the initialize method when calling a class.

Why not just use nil?
def method(required_arg, option1 = nil, option2 = nil)
...
end

There is a method_missing method on ActiveRecord models that you can override to have your class dynamically respond to calls directly. Here's a nice blog post on it.

Related

Rails translate enum in an array

I am currently working on statistics, so I get an array containing all my data. The problem is that this data contains enums and that I would like to translate them without overwriting the rest.
Here is a given example that contains my array (it contains several hundred) :
#<Infosheet id: 90, date: "2018-04-22 00:00:00", number: 7, way: "home", gender: "man", age: "age1", district: "", intercommunal: "", appointment: true, othertype: "", otherorientation: "", user_id: 3, created_at: "2018-04-22 17:51:16", updated_at: "2018-04-22 17:51:16", typerequest_id: 168, orientation_id: 188, info_number: nil, city_id: 105>
I would like to translate the enums of "way" or "gender" or "age", while retaining the rest of the data, because currently, if I make a translation in the console, it crushes everything else.
Do you know how to make that ?
Thanks !
You can just loop over all the enum attributes and get their values. Later you can merge and pass a new hash containing converted values
ENUM_COLUMNS = %i[way gender age] # Equivalent to [:way, :gender, :age]
def convert_enums
overrided_attributes = {}
ENUM_COLUMNS.each { |column| overrided_attributes[column.to_s] = self[column] }
attributes.merge(overrided_attributes)
end
NOTE:
While infosheet.gender returns you male or female
infosheet[:gender] will return you the respective integer value 0 or 1
You can test this if you use translate enum gem :
a = Infosheet.group(:gender).count
{“male”=>30, “female”=>6532}
Create a hash
r = Hash.new
And populate this with :
a.each {|s| puts r[Infosheet.translated_gender(s[0])]=s[1] }
r
result :
{“homme”=>30, “femme”=>6532}

ActiveRecord on local console not working, but works with Heroku

I'm rather baffled right now.
I have a database full of airport data. I'm trying to call certain attributes from different records in my console, but it isn't working. Oddly enough it works fine in my heroku console. For example:
If I put: x = Airport.where(code: "JFK"). When I type "x" in my console I get the entire record for JFK. But, when I type x.runway_length I get a method error.
The odd thing is when I do the same process in my Heroku console, I'll get the runway length (or any other attribute I attach to "x").
What's going on? Am I missing something.
Thanks!
x = Airport.where(code: "LAX")
Airport Load (2.9ms) SELECT "airports".* FROM "airports" WHERE "airports"."code" = 'LAX'
=> #<ActiveRecord::Relation [#<Airport id: 3533, code: "LAX", lat: 33.9456, lon: -118.391, name: "Los Angeles International Airport", city: "Los Angeles", state: "California", country: "United States", woeid: 12520706, tz: "America/Los_Angeles", phone: "", email: "", url: "", runway_length: 12091, elev: 126, icao: "KLAX", direct_flights: 200, carriers: 99, created_at: "2015-08-06 19:10:03", updated_at: "2015-08-17 03:05:53", current_high_temp: 84, current_low_temp: 70>]>
Now, when I try: x.current_high_temp
NoMethodError: undefined method `current_high_temp' for #<ActiveRecord::Relation::ActiveRecord_Relation_Airport:0x007fd89ed33b18>
'Where' returns an array, and you are calling an instance method on it.
Try
x = Airport.where(code: "LAX").first
The problem is the method you are using.
Model#where does not return an object (as expected from active record) for you to work with, instead you should use Model#find_by(attribute: expression)
In your case use:
x = Airport.find_by(code: "LAX")
where(opts = :chain, *rest) public
Returns a new relation, which is the result of filtering the current relation according to the conditions in the arguments.
i.e.
x = Airport.where(code: "LAX")
Airport Load (2.9ms) SELECT "airports".* FROM "airports" WHERE "airports"."code" = 'LAX'
=> #<ActiveRecord::Relation [#<Airport id: 3533, code: "LAX", lat: 33.9456, lon: -118.391, name: "Los Angeles International Airport", city: "Los Angeles", state: "California", country: "United States", woeid: 12520706, tz: "America/Los_Angeles", phone: "", email: "", url: "", runway_length: 12091, elev: 126, icao: "KLAX", direct_flights: 200, carriers: 99, created_at: "2015-08-06 19:10:03", updated_at: "2015-08-17 03:05:53", current_high_temp: 84, current_low_temp: 70>]>
you notice that the Object type is ActiveRecord::Relation instead of Airport and the Airport object is wrapped with array literals [] however, the collection type is not array. So,
Either you want to use
x = Airport.where(code: "LAX").first
or
x = Airport.where(code: "LAX")[0] # not recommended
If 'code' is a unique key the you might use
# this gives the first airport with code = 'LAX'
x = Airport.find_by_code("LAX")
or if you want to see the current_high_temp of all airports then you might want to do this
x = Airport.where(code: "LAX").pluck('current_high_temp')
pluck(*column_names) public Use pluck as a shortcut to select one or
more attributes without loading a bunch of records just to grab the
attributes you want.
Person.pluck(:name)
instead of
Person.all.map(&:name)

Sort an active record based on an array of ids

I have an ActiveRecord #grid.
I have an Hash #numbers.
rails console
1.9.3p385 :086 > Grids
=> Grids(unique_id: integer, price: float, availability: string,
delivery: string, delivery_time: string, delivery_cost: float,
special_offers: text, warranty: text, created_at: datetime, updated_at: datetime)
=> #numbers
=>{7=>114, 9=>21, 12=>7, 13=>31, 14=>51, 16=>43, 18=>48, 21=>6, 22=>18,
24=>69, 27=>47, 30=>47, 32=>31, 33=>36}
#numbers hash is nothing but unique_id => number of clicks
1.9.3p385 :091 > #no = #numbers.sort_by{|k,v| v}.reverse.map{|i| i[0]}
=> [7, 24, 14, 18, 30, 27, 16, 33, 13, 32, 9, 22, 12, 21]
#no is an array of unique_id.
#grid = Grid.all
#grid is an activeRecord, I want to sort this active record based
on the order of #no which is nothing but the unique_id in #grid.
Im trying this,
#grid.sort_by{ |h| #no.index(h.unique_id) }
It's not working, it says,
ArgumentError: comparison of NilClass with 14 failed
I understand there is some nil comparison going on. How to ignore this or is there a better approach?
first create a hash from your grid relation indexed by id, and then perform a mapping from lookups on this index :
grid_index = #grid.index_by &:id
#no.map{|id| grid_index[id] } # possibly compact the resulting array after that
It happens because #grid includes some unique_id which #no does not have.
Replace
#grid = Grid.all
With
#grid = Grid.all(:conditions=>["unique_id in (?)",#no])
Then
#grid.sort_by{ |h| #no.index(h.unique_id) }
Try:
#grid.where(:unique_id => #no).map(&:unique_id)

Ruby on Rails: Updating data problems

Okay so I'm just trying to write some simple code that updates a record given an ID. This is what it looks like.
def updaterecord
bathroom = Bathroom.find(params[:key])
bathroom.name= params[:name],
#bathroom.bathroomtype = params[:bathroomtype],
bathroom.street = params[:street]
#bathroom.city = params[:city],
#bathroom.state = params[:state],
#bathroom.country = params[:country],
#bathroom.postal = params[:postal],
#bathroom.access = params[:access],
#bathroom.directions = params[:directions],
#bathroom.comment = params[:commment],
#bathroom.avail = params[:avail]
bathroom.save
end
The problem is that although I am trying to update each individual attribute they are all getting concatenated to the name field. For example, this code above sets the name field to the name plus the address. I don't know why?
This is what the console looks like if I try to query after doing the update.
Bathroom Load (0.2ms) SELECT "bathrooms".* FROM "bathrooms" WHERE "bathrooms"."ID" = ? LIMIT 1 [["ID", 4017]]
=> #<Bathroom ID: 4017, name: "--- \n- ttyt\n- 113 some\n", bathroomtype: "0", street: "113 some", city: "Portland", state: "OR", country: "United States", postal: "97217", lat: #<BigDecimal:1109f2890,'0.4558056E2',12(12)>, lon: #<BigDecimal:1109f27c8,'-0.122677857E3',12(16)>, access: "0", directions: "", comment: nil, created: "2012-06-08 17:19:03.420329", modifed: "", avail: "1", slug: "", source: "Squat">
And this is what the post values look like:
post values = key=4017&name=www&bathroomtype=0&street=7540 N Interstate Ave&city=Portland&state=OR&country=United States&postal=97217&access=0&directions=&comment=<null>&avail=1
Why can't I get each field to update individually? Sorry I'm confused as to what is going on?
I think you might an unnecessary comma there at the end of the line.
It should just read:
def updaterecord
bathroom = Bathroom.find(params[:key])
bathroom.name= params[:name]
bathroom.street = params[:street]
bathroom.save
end
it doesn't look like you have correctly urlencoded your post values
a simple
puts params.inspect
or
pp params
should log out the params object. You can also use
render :text => params.inspect
to print it out to your result html

ruby change content of a string

I'm doing data processing, one task is to get stats of people distribution. Say for the people of name "john doe", there fall in different states, ca, ar, and ny, and of different age groups, twenties, thirties, etc. {1,2} or {3} is the people's id.
"john doe" => "ca:tw#2{1,2}:th#1{3};ar:tw#1{4}:fi#1{5};ny:tw#1{6};"
Now if I want to get the id of john doe in ca with age tw, how should I get them? Maybe using Regex? And if I want to add a new id to it, say 100, now it becomes
"john doe" => "ca:tw#3{1,2,100}:th#1{3};ar:tw#1{4}:fi#1{5};ny:tw#1{6};"
how should I do that?
Thanks!
If you want to stick with string manipulation, you can use regex and gsub.
Here is one way to do it. It could use some clean up (eg error handling, re-factoring, etc.), but I think it would get you started.
def count(details, location, age_group)
location_details = /#{location}(.+?);/.match(details)[1]
age_count = /#{age_group}#(\d+)\{/.match(details)[1]
return age_count.to_i
end
def ids(details, location, age_group)
location_details = /#{location}(.+?);/.match(details)[1]
age_ids = /#{age_group}#\d+\{(.+?)\}/.match(details)[1]
return age_ids
end
def add(details, location, age_group, new_id)
location_details = /#{location}(.+?);/.match(details)[1]
new_count = count(details, location, age_group) + 1
new_ids = ids(details, location, age_group) + ',' + new_id
location_details.gsub!(/#{age_group}#\d+\{(.+?)\}/, "#{age_group}##{new_count}{#{new_ids}}")
details.gsub!(/#{location}(.+?);/, "#{location}#{location_details};")
end
You can see it produces the results you wanted (at least functionally, not sure about performance):
names = {"john doe" => "ca:tw#2{1,2}:th#1{3};ar:tw#1{4}:fi#1{5};ny:tw#1{6};"}
puts count(names["john doe"], 'ca', 'tw')
#=> 2
puts ids(names["john doe"], 'ca', 'tw')
#=> 1,2
names["john doe"] = add(names["john doe"], 'ca', 'tw', '100')
puts names["john doe"]
#=> ca:tw#3{1,2,100}:th#1{3};ar:tw#1{4}:fi#1{5};ny:tw#1{6};
It doesn't make sense to use a string for this inside the program. You may read the data from a string as it is stored, or write it back out that way, but you should store it in a manner that's easy to manipulate. For instance:
data = {
"john doe" => {
"ca" => {
"tw" => [1,2],
"th" => [3]
},
"ar" => {
"tw" => [4],
"fi" => [5]
},
"ny" => {
"tw" => [6]
}
}
}
Given that, the ids of the California John Doe's in their 20's are data['john doe']['ca']['tw']. The number of such John Doe's is data['john doe']['ca']['tw'].length; the first id is data['john doe']['ca']['tw'][0], and the second is data['john doe']['ca']['tw'][1]. You could add id 100 to it with data['john doe']['ca']['tw'] << 100; 100 would then be the value of data['john doe']['ca']['tw'][2].
If I were writing this, though, I would probably use actual numbers for the age-range keys (20, 30, 50) instead of those obscure letter prefixes.

Resources