Use a string to access a local variable by name - ruby

I'm new to this but I have the following code:
when /^read (.+)$/
puts "Reading #{$1}:"
puts $1.description.downcase
I would like to use $1 as a variable that I can call methods on, currently the interpreter returns a "NoMethodError: undefined method 'description' for "Door":String".
Edit:
For example:
door = Item.new( :name => "Door", :description => "a locked door" )
key = Item.new( :name => "Key", :description => "a key" )

You need to provide more details of your code setup to get a good answer (or for me to figure out which question this is a duplicate of :). What kind of variables are referenced by $1? Here are some guesses:
If this is actually a method on the same instance, you can invoke this method by:
# Same as "self.foo" if $1 is "foo"
self.send($1).description.downcase
If these are instance variables, then:
# Same as "#foo.description.downcase"
instance_variable_get(:"##{$1}").description.downcase
If these are local variables, you can't do it directly, and you should change your code to use a Hash:
objs = {
'foo' => ...,
'key' => Item.new( :name => "Key", :description => "a key" )
}
objs['jim'] = ...
case some_str
when /^read (.+)$/
puts "Reading #{$1}:"
puts objs[$1].description.downcase
end

I guess you matched a string like "read Door" with /^read (.+)$/. So $1 = "Door" and it raised the above error. If you want to downcase that string, just use:
$1.downcase

Related

How to access a symbol hash key using a variable in Ruby

I have an array of hashes to write a generic checker for, so I want to pass in the name of a key to be checked. The hash was defined with keys with symbols (colon prefixes). I can't figure out how to use the variable as a key properly. Even though the key exists in the hash, using the variable to access it results in nil.
In IRB I do this:
>> family = { 'husband' => "Homer", 'wife' => "Marge" }
=> {"husband"=>"Homer", "wife"=>"Marge"}
>> somevar = "husband"
=> "husband"
>> family[somevar]
=> "Homer"
>> another_family = { :husband => "Fred", :wife => "Wilma" }
=> {:husband=>"Fred", :wife=>"Wilma"}
>> another_family[somevar]
=> nil
>>
How do I access the hash key through a variable? Perhaps another way to ask is, how do I coerce the variable to a symbol?
You want to convert your string to a symbol first:
another_family[somevar.to_sym]
If you want to not have to worry about if your hash is symbol or string, simply convert it to symbolized keys
see: How do I convert a Ruby hash so that all of its keys are symbols?
You can use the Active Support gem to get access to the with_indifferent_access method:
require 'active_support/core_ext/hash/indifferent_access'
> hash = { somekey: 'somevalue' }.with_indifferent_access
=> {"somekey"=>"somevalue"}
> hash[:somekey]
=> "somevalue"
> hash['somekey']
=> "somevalue"
Since your keys are symbols, use symbols as keys.
> hash = { :husband => 'Homer', :wife => 'Marge' }
=> {:husband=>"Homer", :wife=>"Marge"}
> key_variable = :husband
=> :husband
> hash[key_variable]
=> "Homer"
If you use Rails with ActiveSupport, then do use HashWithIndifferentAccess for flexibility in accessing hash with either string or symbol.
family = HashWithIndifferentAccess.new({
'husband' => "Homer",
'wife' => "Marge"
})
somevar = "husband"
puts family[somevar]
#Homer
somevar = :husband
puts family[somevar]
#Homer
The things that you see as a variable-key in the hash are called Symbol is a structure in Ruby. They're primarily used either as hash keys or for referencing method names. They're immutable, and Only one copy of any symbol exists at a given time, so they save memory.
You can convert a string or symbol with .to_sym or a symbol to string with .to_s to illustrate this let me show this example:
strings = ["HTML", "CSS", "JavaScript", "Python", "Ruby"]
symbolArray = [:HTML, :CSS, :JavaScript, :Python, :Ruby]
# Add your code below!
symbols = Array.new
strings.each {|x|
symbols.push(x.to_sym)
}
string = Array.new
symbolArray .each {|x|
string.push(x.to_s)
}
print symbols
print string
the result would be:
[:HTML, :CSS, :JavaScript, :Python, :Ruby]
["HTML", "CSS", "JavaScript", "Python", "Ruby"]
In ruby 9.1 you would see the symbols with the colons (:) in the right instead:
movies = { peter_pan: "magic dust", need_4_speed: "hey bro", back_to_the_future: "hey Doc!" }
I just wanted to make this point a litter more didactic so who ever is reading this can used.
One last thing, this is another way to solve your problem:
movie_ratings = {
:memento => 3,
:primer => 3.5,
:the_matrix => 3,
}
# Add your code below!
movie_ratings.each_key {|k|
puts k.to_s
}
result:
memento
primer
the_matrix

Reading and writing Sinatra params using symbols, e.g. params[:id]

My form receives data via POST. When I do puts params I can see:
{"id" => "123", "id2" => "456"}
now the commands:
puts params['id'] # => 123
puts params[:id] # => 123
params['id'] = '999'
puts params # => {"id" => "999", "id2" => "456"}
but when I do:
params[:id] = '888'
puts params
I get
{"id" => "999", "id2" => "456", :id => "888"}
In IRB it works fine:
params
# => {"id2"=>"2", "id"=>"1"}
params[:id]
# => nil
params['id']
# => "1"
Why can I read the value using :id, but not set the value using that?
Hashes in Ruby allow arbitrary objects to be used as keys. As strings (e.g. "id") and symbols (e.g. :id) are separate types of objects, a hash may have as a key both a string and symbol with the same visual contents without conflict:
irb(main):001:0> { :a=>1, "a"=>2 }
#=> {:a=>1, "a"=>2}
This is distinctly different from JavaScript, where the keys for objects are always strings.
Because web parameters (whether via GET or POST) are always strings, Sinatra has a 'convenience' that allows you to ask for a parameter using a symbol and it will convert it to a string before looking for the associated value. It does this by using a custom default_proc that calls to_s when looking for a value that does not exist.
Here's the current implementation:
def indifferent_hash
Hash.new {|hash,key| hash[key.to_s] if Symbol === key }
end
However, it does not provide a custom implementation for the []=(key, val) method, and thus you can set a symbol instead of the string.

Refactor ruby on rails model

Given the following code,
How would you refactor this so that the method search_word has access to issueid?
I would say that changing the function search_word so it accepts 3 arguments or making issueid an instance variable (#issueid) could be considered as an example of bad practices, but honestly I cannot find any other solution. If there's no solution aside from this, would you mind explaining the reason why there's no other solution?
Please bear in mind that it is a Ruby on Rails model.
def search_type_of_relation_in_text(issueid, type_of_causality)
relation_ocurrences = Array.new
keywords_list = {
:C => ['cause', 'causes'],
:I => ['prevent', 'inhibitors'],
:P => ['type','supersets'],
:E => ['effect', 'effects'],
:R => ['reduce', 'inhibited'],
:S => ['example', 'subsets']
}[type_of_causality.to_sym]
for keyword in keywords_list
relation_ocurrences + search_word(keyword, relation_type)
end
return relation_ocurrences
end
def search_word(keyword, relation_type)
relation_ocurrences = Array.new
#buffer.search('//p[text()*= "'+keyword+'"]/a').each { |relation|
relation_suggestion_url = 'http://en.wikipedia.org'+relation.attributes['href']
relation_suggestion_title = URI.unescape(relation.attributes['href'].gsub("_" , " ").gsub(/[\w\W]*\/wiki\//, ""))
if not #current_suggested[relation_type].include?(relation_suggestion_url)
if #accepted[relation_type].include?(relation_suggestion_url)
relation_ocurrences << {:title => relation_suggestion_title, :wiki_url => relation_suggestion_url, :causality => type_of_causality, :status => "A", :issue_id => issueid}
else
relation_ocurrences << {:title => relation_suggestion_title, :wiki_url => relation_suggestion_url, :causality => type_of_causality, :status => "N", :issue_id => issueid}
end
end
}
end
If you need additional context, pass it through as an additional argument. That's how it's supposed to work.
Setting #-type instance variables to pass context is bad form as you've identified.
There's a number of Ruby conventions you seem to be unaware of:
Instead of Array.new just use [ ], and instead of Hash.new use { }.
Use a case statement or a constant instead of defining a Hash and then retrieving only one of the elements, discarding the remainder.
Avoid using return unless strictly necessary, as the last operation is always returned by default.
Use array.each do |item| instead of for item in array
Use do ... end instead of { ... } for multi-line blocks, where the curly brace version is generally reserved for one-liners. Avoids confusion with hash declarations.
Try and avoid duplicating large chunks of code when the differences are minor. For instance, declare a temporary variable, conditionally manipulate it, then store it instead of defining multiple independent variables.
With that in mind, here's a reworking of it:
KEYWORDS = {
:C => ['cause', 'causes'],
:I => ['prevent', 'inhibitors'],
:P => ['type','supersets'],
:E => ['effect', 'effects'],
:R => ['reduce', 'inhibited'],
:S => ['example', 'subsets']
}
def search_type_of_relation_in_text(issue_id, type_of_causality)
KEYWORDS[type_of_causality.to_sym].collect do |keyword|
search_word(keyword, relation_type, issue_id)
end
end
def search_word(keyword, relation_type, issue_id)
relation_occurrences = [ ]
#buffer.search(%Q{//p[text()*= "#{keyword}'"]/a}).each do |relation|
relation_suggestion_url = "http://en.wikipedia.org#{relation.attributes['href']}"
relation_suggestion_title = URI.unescape(relation.attributes['href'].gsub("_" , " ").gsub(/[\w\W]*\/wiki\//, ""))
if (!#current_suggested[relation_type].include?(relation_suggestion_url))
occurrence = {
:title => relation_suggestion_title,
:wiki_url => relation_suggestion_url,
:causality => type_of_causality,
:issue_id => issue_id
}
occurrence[:status] =
if (#accepted[relation_type].include?(relation_suggestion_url))
'A'
else
'N'
end
relation_ocurrences << occurrence
end
end
relation_occurrences
end

Referencing hash item inside itself

I've got a pretty short question. Is it possible to initialize a hash with something like this:
row = {
:title => "row title",
:slug => row[:title].paremeterize
}
In other words, could I somehow reference an unitialized hash inside itself or I have to do it this way:
row = {
:title => "row title"
}
row[:slug] = row[:title].paremeterize
Thanks for the comments. Of course, this code won't work. I asked if there is a similar way, maybe with a different syntax. Ruby has been full of surprises for me :)
You're going about this in a rather strange way. Try to think about what you are doing when you run into cases where you are trying to use the language in ways that are rarely documented (or impossible).
title = "foobar"
row = {
:title => title,
:slug => title.parameterize
}
Even better…
class Row
attr_accessor :title
def slug; title.parameterize; end
end
foo = Row.new :title => 'foo bar'
foo.slug #=> "foo-bar"
If you run the following in IRB,
row = {
:title => "row title",
:slug => row[:title]
}
you get the error NoMethodError: undefined method '[]' for nil:NilClass. So no you can't do that, given that row hasn't been fully initialized at that point and is a nil object.

Question on Ruby collect method

I have an array of hashes
Eg:
cars = [{:company => "Ford", :type => "SUV"},
{:company => "Honda", :type => "Sedan"},
{:company => "Toyota", :type => "Sedan"}]
# i want to fetch all the companies of the cars
cars.collect{|c| c[:company]}
# => ["Ford", "Honda", "Toyota"]
# i'm lazy and i want to do something like this
cars.collect(&:company)
# => undefined method `company'
I was wondering if there is a similar shortcut to perform the above.
I believe your current code cars.collect{|c| c[:company]} is the best way if you're enumerating over an arbitrary array. The method you would pass in via the & shortcut would have to be a method defined on Hash since each object in the array is of type Hash. Since there is no company method defined for Hash you get the "undefined method 'company'" error.
You could use cars.collect(&:company) if you were operating on an Array of Cars though, because each object passed into the collect block would be of type Car (which has the company method available). So maybe you could modify your code so that you use an array of Cars instead.
You could convert the hashes to OpenStructs.
require 'ostruct'
cars = [{:company => "Ford", :type => "SUV"},
{:company => "Honda", :type => "Sedan"},
{:company => "Toyota", :type => "Sedan"}]
cars = cars.map{|car| OpenStruct.new(car)}
p cars.map( &:company )
#=> ["Ford", "Honda", "Toyota"]
It's impossible to use in your case, because in collect you use method [] and argument :company. The construction &:company takes labels :company and converts to Proc, so it's only one argument - the name of method.
Unfortunately Ruby hashes can't do that. Clojure maps on the other hand have functions for each key which return the corresponding value, which would be easy enough to do if you are so inclined (you should also add the corresponding respond_to? method):
>> class Hash
.. def method_missing(m)
.. self.has_key?(m) ? self[m] : super
.. end
.. end #=> nil
>> cars.collect(&:company) #=> ["Ford", "Honda", "Toyota"]
>> cars.collect(&:compay)
NoMethodError: undefined method `compay' for {:type=>"SUV", :company=>"Ford"}:Hash
Note: I'm not advising this, I'm just saying it's possible.
Another horrible monkeypatch you shouldn't really use:
class Symbol
def to_proc
if self.to_s =~ /bracket_(.*)/
Proc.new {|x| x[$1.to_sym]}
else
Proc.new {|x| x.send(self)}
end
end
end
cars = [{:company => "Ford", :type => "SUV"},
{:company => "Honda", :type => "Sedan"},
{:company => "Toyota", :type => "Sedan"}]
cars.collect(&:bracket_company)

Resources