Alternative to logical OR, where false is treated as truthy? - ruby

So I have some code that gets values from a couple sources in a fixed priority order. It looks like this:
{ foo: true
bar: 42
}.each_pair do |attrib,default|
instance_variable_set :"##{attrib}",data[attrib] || template[attrib] || otherdata[attrib] || default
end
As you can probably tell, trying to override a true default with false won't work. How would I fix this without creating a horrible mess? (I do know how to fix it with a horrible mess, but I'm fairly certain there is something nice in the standard library for this exact situation, I just don't remember what it is called and can't think of a good search term)

If you're looking for the first non-nil value:
instance_variable_set :"##{attrib}", [ data[attrib], template[attrib], otherdata[attrib], default ].compact.first
Not the most efficient method, but gets the job done and works well if not exercised aggressively.
A slightly more forgiving version:
instance_variable_set :"##{attrib}", [ data[attrib], template[attrib], otherdata[attrib], default ].find { |v| !v.nil? }
This scans for the first non-nil value and returns that. It doesn't produce an intermediate array like compact does.
What you really want is a more forgiving method:
def instance_variable_set_from(attrib, *sources)
instance_variable_set(:"##{attrib}", sources.find { |v| !v.nil? }
end
Which is more self-explanatory when used:
instance_variable_set_from(attrib, data[attrib], template[attrib], otherdata[attrib], default)

Assuming that data, template and otherdata are all hashes, you could use fetch. It returns the value if it is contained in the hash, or the block's result otherwise:
value = data.fetch(attrib) { template.fetch(attrib) { otherdata.fetch(attrib, default) } }
instance_variable_set :"##{attrib}", value
or alternatively find the first hash containing the key and fetch its result, falling back to default:
value = [data, template, other_data].find { |h| h.key?(attrib) }.fetch(attrib, default)
Note that this would also allow nil values.

Related

Searching a Hash

I'm trying to complete this Codewars Challenge and I'm confused as to where I'm going wrong. Could someone please give me a hand?
The question provides a "database" of translations for Welcome, and the instructions say:
Think of a way to store the languages as a database (eg an object). The languages are listed below so you can copy and paste!
Write a 'welcome' function that takes a parameter 'language' (always a string), and returns a greeting - if you have it in your database. It should default to English if the language is not in the database, or in the event of an invalid input.
My attempt:
def greet(language)
greeting = { 'english'=>'Welcome',
'czech'=>'Vitejte',
'danish'=>'Velkomst',
'dutch'=>'Welkom',
'estonian'=>'Tere tulemast',
'finnish'=>'Tervetuloa',
'flemish'=>'Welgekomen',
'french'=>'Bienvenue',
'german'=>'Willkommen',
'irish'=>'Failte',
'italian'=>'Benvenuto',
'latvian'=>'Gaidits',
'lithuanian'=>'Laukiamas',
'polish'=>'Witamy',
'spanish'=>'Bienvenido',
'swedish'=>'Valkommen',
'welsh'=>'Croeso'
}
greeting.key?(language) ? greeting.each { |k, v| return v if language == k } : 'IP_ADDRESS_INVALID'
end
To my eyes when I run my code through the IDE it seems to be working as per request but I guess I must be wrong somehow.
It's telling me it :
Expected: "Laukiamas", instead got: "Welcome"
But when I type:
p greet("lithuanian")
I get Laukiamas.
You can provide you greeting hash with a default value. It is as simple as
greeting.default = "Welcome"
This enhanced hash does all the work for you. Just look up the key; when it is not there you'll get "Welcome".
Preface
First of all, please don't post links to exercises or homework questions. Quote them in your original question to avoid link rot or additional create work for people trying to help you out.
Understanding the Problem Defined by the Linked Question
Secondly, you're misunderstanding the core question. The requirement is basically to return the Hash value for a given language key if the key exists in the Hash. If it doesn't, then return the value of the 'english' key instead. Implicit in the exercise is to understand the various types of improper inputs that would fail to find a matching key; the solution below addresses most of them, and will work even if your Ruby has frozen strings enabled.
A Working Solution
There are lots of ways to do this, but here's a simple example that will handle invalid keys, nil as a language argument, and abstract away capitalization as a potential issue.
DEFAULT_LANG = 'english'
TRANSLATIONS = {
'english' => 'Welcome',
'czech' => 'Vitejte',
'danish' => 'Velkomst',
'dutch' => 'Welkom',
'estonian' => 'Tere tulemast',
'finnish' => 'Tervetuloa',
'flemish' => 'Welgekomen',
'french' => 'Bienvenue',
'german' => 'Willkommen',
'irish' => 'Failte',
'italian' => 'Benvenuto',
'latvian' => 'Gaidits',
'lithuanian' => 'Laukiamas',
'polish' => 'Witamy',
'spanish' => 'Bienvenido',
'swedish' => 'Valkommen',
'welsh' => 'Croeso'
}
# Return a translation of "Welcome" into the language
# passed as an argument.
#
# #param language [String, #to_s] any object that can
# be coerced into a String, and therefore to
# String#downcase
# #return [String] a translation of "Welcome" or the
# string-literal +Welcome+ if no translation found
def greet language
language = language.to_s.downcase
TRANSLATIONS.fetch language, TRANSLATIONS[DEFAULT_LANG]
end
# Everything in the following Array of examples except
# +Spanish+ should return the Hash value for +english+.
['Spanish', 'Español', 123, nil].map { greet(_1) }
This will correctly return:
#=> ["Bienvenido", "Welcome", "Welcome", "Welcome"]
because only Spanish (when lower-cased) will match any of the keys currently defined in the TRANSLATIONS Hash. All the rest will use the default value defined for the exercise.
Test Results
Since there are some RSpec tests included with the linked question:
describe "Welcome! Translation" do
it "should translate input" do
Test.assert_equals(greet('english'), 'Welcome', "It didn't work out this time, keep trying!");
Test.assert_equals(greet('dutch'), 'Welkom', "It didn't work out this time, keep trying!");
Test.assert_equals(greet('IP_ADDRESS_INVALID'), 'Welcome', "It didn't work out this time, keep trying!")
end
end
The code provided not only passes the provided tests, but it also passes a number of other edge cases not defined in the unit tests. When run against the defined tests, the code above passes cleanly:
If this is homework, then you might want to create additional tests to cover all the various edge cases. You might also choose to refactor to less idiomatic code if you want more explanatory variables, more explicit intermediate conversions, or more explicit key handling. The point of good code is to be readable, so be as explicit in your code and as thorough in your tests as you need to be in order to make debugging easier.

How to parse two elements from a list to make a new one

I have this input repeated in 1850 files:
[
{
"id"=>66939,
"login"=>"XXX",
"url"=>"https://website.com/XX/users/XXX"
},
...
{}
]
And I wanted to make a list in a way that by looking for the login I can retrieve the ID using a syntax like:
users_list[XXX]
This is my desired output:
{"XXX"=>"66570", "XXX"=>"66570", "XXX"=>"66570", "XXX"=>"66570", ... }
My code is:
i2 = 1
while i2 != users_list_raw.parsed.count
temp_user = users_list_raw.parsed[i2]
temp_user_login = temp_user['login']
temp_user_id = temp_user['id']
user = {
temp_user_login => temp_user_id
}
users_list << user
i2 += 1
end
My output is:
[{"XXX":66570},{"XXX":66569},{"XXX":66568},{"XXX":66567},{"XXX":66566}, ... {}]
but this is not what I want.
What's wrong with my code?
hash[key] = value to add an entry in a hash. So I guess in your case users_list[temp_user_login] = temp_user_id
But I'm unsure why you'd want to do that. I think you could look up the id of a user by having the login with a statement like:
login = XXX
user = users_list.select {|user| user["login"] == login}.first
id = user["id"]
and maybe put that in a function get_id(login) which takes the login as its parameter?
Also, you might want to look into databases if you're going to manipulate large amounts of data like this. ORMs (Object Relational Mappers) are available in Ruby such as Data Mapper and Active Record (which comes bundled with Rails), they allow you to "model" the data and create Ruby objects from data stored in a database, without writing SQL queries manually.
If your goal is to lookup users_list[XXX] then a Hash would work well. We can construct that quite simply:
users_list = users_list_raw.parsed.each.with_object({}) do |user, list|
list[user['login']] = user['id']
end
Any time you find yourself writing a while loop in Ruby, there might be a more idiomatic solution.
If you want to keep track of a mapping from keys to values, the best data structure is a hash. Be aware that assignment via the array operator will replace existing values in the hash.
login_to_id = {}
Dir.glob("*.txt") { |filename| # Use Dir.glob to find all files that you want to process
data = eval(File.read(filename)) # Your data seems to be Ruby encoded hash/arrays. Eval is unsafe, I hope you know what you are doing.
data.each { |hash|
login_to_id[hash["login"]] = hash["id"]
}
}
puts login_to_id["XXX"] # => 66939

Access variable hash depth values with square brackets notation

Given this hash:
hash1= { node1: { node2: { node3: { node4: { node5: 1 } } } } }
We access inside nodes with square brackets like this:
hash1[:node1][:node2][:node3][:node4]
Now I have a hash that I know will always be nested as it is an XML response from a SOAP webservice, but neither the depth of the hash nor the names of the nodes stay the same. So it would be nice if I could ask the user of my application for the hash depth and store it in a variable. And then be able to do hash1[:hash_depth] and achieve the same result as above.
I have accomplished what I want by the following code:
str = 'node1,node2,node3,node4'
str_a = str.split(',')
hash_copy = hash1
str_a.each { |s| hash_copy = hash_copy.[](s.to_sym) }
hash_copy
=> {:node5=>1}
hash1[:node1][:node2][:node3][:node4]
=> {:node5=>1}
that is asking the user to enter the hash depth separated by commas, store it in a string, split it, make an array, clone the original hash, go down each level and modify the hash till I get to the desired node. Is there a way to do it with the square brackets notation and using a variable to store the depth without modifying the hash or needing to clone it?
Edit:
someone answered with the following (can't see his post anymore???)
hash_depth="[:node1][:node2][:node3][:node4]"
eval "hash1#{hash_depth}"
Although eval does everything you need, there is another approach, since you already have the working code for comma-separated list:
hash_depth="[:node1][:node2][:node3][:node4]"
csh = hash_depth.gsub(/\A\[:|\]\[:|\]\Z/, { '][:' => ',' })
#⇒ "node1,node2,node3,node4"
And now you are free to apply your existing function to csh.
If this is a webapp, I think you should prepare a list of short textareas, which starts with a single text item, and the user can keep adding a new item to the list by clicking on a button. The areas will be filled by the user, and will be sent.
Then, you will probably receive this through some serialized form. You decode this to get an array of strings:
str_a = ["node1", "node2", "node3", "node4"]
and you can reach the inner element by doing:
str_a.inject(hash1){|h, s| h[s.to_sym]} #=> {:node5 => 1}

ruby - calling a method on a dynamically named object

I have an array of strings, that represent existing object names.
JoesDev = Dev.new
MarksDev = Dev.new
SamsDev = Dev.new
devices=['JoesDev', 'MarksDev', 'SamsDev' ]
i'd like to iterate over the devices array, while calling a method on the objects that each item in the array is named after.
i.e;
JoesDev.method_name
MarksDev.method_name
SamsDev.method_name
how can i do this? thx.
devices.each{|name| self.class.const_get(name).method_name}
You can use the const_get method from Module to have Ruby return the constant with the given name. In your case, it will return the Dev instance for whatever device name you give it.
Using .each to iterate the items, your code could look like
devices.each do |device_name|
device = self.class.const_get(device_name)
device.method_name
end
# Which can be shortened to
devices.each{ |dev| self.class.const_get(dev).method_name }
However, there are better ways to implement this type of thing. The most common way is using a Hash. In your example, the list of devices could look something like
devices = {
joe: Dev.new,
mark: Dev.new,
sam: Dev.new
}
Then, iterating over the devices is as simple as
devices.each do |dev|
dev.method_name
end
# Or
devices.each{ |dev| dev.method_name }
Extra: If you want to get a little fancy, you can use the block version of Hash::new to make adding new devices extremely simple.
# Create the hash
devices = Hash.new{ |hash, key| hash[key] = Dev.new }
# Add the devices
devices['joe']
devices['mark']
devices['sam']
This kind of hash works exactly the same as the one shown above, but will create a new entry if the given key cannot be found in the hash. A potential problem with this design, then, is that you can accidentally add new devices if you make a typo. For example
devices['jon'] # This would make a new Dev instance, which may be undesirable.
Well one way is surely to use eval, a method that allows you to execute arbitrary strings as if they were code.
So, in your example:
var_names.each{ |var_name| eval("#{var_name}.some_method") }
Needless to say, it is very dangerous to let unfiltered strings to be used as code, very bad things™ may happen!

How do I elegantly handle nil cases in my array assignment - Ruby?

So I am pushing some elements on my array like this:
upd_city_list << [ j.children[0].text.strip!.gsub(/\s+\W/, ''), j.children[1].text, j.children[1][:href] ]
The above is in an iterator (hence the use of j).
The issue is that from time to time, the j.children[0].text turns up as nil, and Ruby doesn't like that.
I could add a bunch of if statements before this assignment, but that seems a bit inelegant to me.
How do I handle nil cases in this situation in an elegant way?
One possible solution is, when there is a nil value, just push the string none onto the array....but what would that look like?
Thanks.
Edit1:
This is the error I am getting:
NoMethodError: private method ‘gsub’ called for nil:NilClass
The real problem is that strip! returns nil when there are no changes to the string. Your text method is returning a string, it is your strip! method is returning nil. I don't know why it does this. I dislike it, too.
This case of the problem will go away if you just change strip! to strip
In a more general sense, you might create an object to return the array for you. You don't want to go changing (what I assume is) Nokogiri, but you can wrap it in something to hide the train wrecks that result.
You should replace j.children[0].text.strip! with one of two things:
(j.children[0].text || 'none').strip
or
j.children[0].text.to_s.strip
These will, of course, have different effects when the text is nil. I think your ACTUAL problem is that strip! was returning nil, and that should have been obvious to you from the error message.
This might be the case for one to use null object programming pattern. Nil is not a good null object. Try reading here and here. Null object is the elegant way.
nil or a_string will be a_string
so what about (j.children[0].text or 'none')
If you're in rails, this is a great use for the try method.
Also seems that your strip and gsub are redundent. Please consider this implementation:
descriptive_name_1 = j.children[0].text.try(:strip)
descriptive_name_2 = j.children[1].text
descriptive_name_3 = j.children[1][:href]
updated_city_list << [ descriptive_name_1 , descriptive_name_2, descriptive_name_3 ]
w/o try
descriptive_name_1 = j.children[0].text.to_s.strip
descriptive_name_2 = j.children[1].text
descriptive_name_3 = j.children[1][:href]
updated_city_list << [ descriptive_name_1 , descriptive_name_2, descriptive_name_3 ]
If you're in the rails environment you could try try method: https://github.com/rails/rails/blob/82d41c969897cca28bb318f7caf301d520a2fbf3/activesupport/lib/active_support/core_ext/object/try.rb#L50

Resources