How do I subgroup this hash that has already been grouped? - ruby

I have a set of word strings which I am turning into a hash, grouped by the size of the string. I am doing this by:
hash = set.group_by(&:size)
resulting in
hash = {5=>[apple, andys, throw, balls], 7=>[bananas, oranges]}
I want to further group the hash values by first letter, so the the end results looks like:
hash = {5=>{a=>[apple, andys],b=>[balls],t=>[throw]}, 7=>{b=>[bananas], o=>[oranges]}}
I tried putting
hash.each_value do | value |
value = value.group_by(&:chr)
end
after the first group_by but that only seems to return the original hash. I am admittedly a ruby beginner so I'm not sure if I could do this in one fell swoop, or exactly how (&:size) notation works, if I were asked to write it out. Thoughts?

To update your hash you need to do like this
hash.each do |key, value|
hash[key] = value.group_by(&:chr)
end

I'd keep the whole computation functional:
>> Hash[set.group_by(&:size).map { |k, vs| [k, vs.group_by(&:chr)] }]
=> {5=>{"a"=>["apple", "andys"], "t"=>["throw"], "b"=>["balls"]},
7=>{"b"=>["bananas"], "o"=>["oranges"]}}

Related

Convert dot notation keys to tree-structured YAML in Ruby

I've sent my I18n files to be translated by a third party. Since my translator is not computer savvy we made a spreadsheet with the keys, they where sent in dot notation and the values translated.
For example:
es.models.parent: "Pariente"
es.models.teacher: "Profesor"
es.models.school: "Colegio"
How can I move that into a YAML file?
UPDATE: Just like #tadman said, this already is YAML. So if you are with the, you are just fine.
So we will focus this question if you would like to have the tree structure for YAML.
The first thing to do is transform this into a Hash.
So the previous info moved into this:
tr = {}
tr["es.models.parent"] = "Pariente"
tr["es.models.teacher"] = "Profesor"
tr["es.models.school"] = "Colegio"
Then we just advanced creating a deeper hash.
result = {} #The resulting hash
tr.each do |k, value|
h = result
keys = k.split(".") # This key is a concatenation of keys
keys.each_with_index do |key, index|
h[key] = {} unless h.has_key? key
if index == keys.length - 1 # If its the last element
h[key] = value # then we only need to set the value
else
h = h[key]
end
end
end;
require 'yaml'
puts result.to_yaml #Here it is for your YAMLing pleasure

In Ruby, group_by where I know there's only 1 element per group

I have a CSV file where one column is a primary key. When I do this:
CSV.read(ARGV[0], headers: true).group_by {|r| r['myKey']}
I get a hash table from key to a list of rows, where the list is always length 1.
Is there a version of group_by which asserts that there's only a single value per key, and creates a hash from key to that single value?
Failing that, is there something like .first which asserts that there's exactly one element in the array/enumerable? I like my scripts to fail when my assumptions are wrong, rather than silently return the wrong thing.
If you use Rails you can use index_by method.
If you know the values r['myKey'] are unique, there's no point in using group_by. As I understand the question, you could do this:
rows = CSV.read(ARGV[0], headers: true)
Hash[rows.map { |r| r['myKey'] }.zip(rows)]
In Ruby 2.0+ the second row could be written:
rows.map { |r| r['myKey'] }.zip(rows).to_h
No. I don't believe there is. But you can solve your problem with each_with_object like so:
CSV.
read(ARGV[0], headers: true).
each_with_object({}) do |r, hash|
key = r['myKey']
value = r
hash[key] = value
end
It's a shame Ruby doesn't have this. Here's what I decided to go on, based on Humza's answer:
module Enumerable
def group_by_uniq
each_with_object({}) do |value, hash|
key = yield value
raise "Multiple values for key \"{key}\"!" unless ! hash.key?(key)
hash[key] = value
end
end
end
If you use your code in you first example you can run this code to check that all hashes are of length 1:
raise 'multiple entries per key!' unless my_hash.values.any?{|val| val.size!=1}
IF you can get the keys into an array you can check that they do not iclude duplicates by:
raise 'multiple entries per key!' unless my_keys.uniq.size == my_keys.size

Ruby Hash destructive vs. non-destructive method

Could not find a previous post that answers my question...I'm learning how to use destructive vs. non-destructive methods in Ruby. I found an answer to the exercise I'm working on (destructively adding a number to hash values), but I want to be clear on why some earlier solutions of mine did not work. Here's the answer that works:
def modify_a_hash(the_hash, number_to_add_to_each_value)
the_hash.each { |k, v| the_hash[k] = v + number_to_add_to_each_value}
end
These two solutions come back as non-destructive (since they all use "each" I cannot figure out why. To make something destructive is it the equals sign above that does the trick?):
def modify_a_hash(the_hash, number_to_add_to_each_value)
the_hash.each_value { |v| v + number_to_add_to_each_value}
end
def modify_a_hash(the_hash, number_to_add_to_each_value)
the_hash.each { |k, v| v + number_to_add_to_each_value}
end
The terms "destructive" and "non-destructive" are a bit misleading here. Better is to use the conventional "in-place modification" vs. "returns a copy" terminology.
Generally methods that modify in-place have ! at the end of their name to serve as a warning, like gsub! for String. Some methods that pre-date this convention do not have them, like push for Array.
The = performs an assignment within the loop. Your other examples don't actually do anything useful since each returns the original object being iterated over regardless of any results produced.
If you wanted to return a copy you'd do this:
def modify_a_hash(the_hash, number_to_add)
Hash[
the_hash.collect do |k, v|
[ k, v + number_to_add ]
end
]
end
That would return a copy. The inner operation collect transforms key-value pairs into new key-value pairs with the adjustment applied. No = is required since there's no assignment.
The outer method Hash[] transforms those key-value pairs into a proper Hash object. This is then returned and is independent of the original.
Generally a non-destructive or "return a copy" method needs to create a new, independent version of the thing it's manipulating for the purpose of storing the results. This applies to String, Array, Hash, or any other class or container you might be working with.
Maybe this slightly different example will be helpful.
We have a hash:
2.0.0-p481 :014 > hash
=> {1=>"ann", 2=>"mary", 3=>"silvia"}
Then we iterate over it and change all the letters to the uppercase:
2.0.0-p481 :015 > hash.each { |key, value| value.upcase! }
=> {1=>"ANN", 2=>"MARY", 3=>"SILVIA"}
The original hash has changed because we used upcase! method.
Compare to method without ! sign, that doesn't modify hash values:
2.0.0-p481 :017 > hash.each { |key, value| value.downcase }
=> {1=>"ANN", 2=>"MARY", 3=>"SILVIA"}

How i can sort this hash by created of and key ,value pair

Hi i am little struggle to make this hash and sort it by created of and key , value pair.
Here is my code
hash_answers = {}
unless answers.blank?
answers.each_with_index do |ans ,index|
voted_up_users = ans.votes_up_by_all_users(ans)
voted_down_users = ans.votes_down_by_all_users(ans)
hash_answers[ans.id] = voted_up_users.count -voted_down_users.count #line one
hash_answers[index+1] = ans.created_at # line 2
end
end
if i have line 1 only in code not line 2 then this below code work fine for me
#answers = hash_answers.sort_by { |key, value| value }.reverse
but i also want to sort it by craeted_at
How i can achive this or make hash in another way
Any help will be most appreciated
Thanks
answers.sort_by do |ans|
[ans.net_votes, ans.created_at]
end
Then in your Answers class
def net_votes
votes_up_by_all_users - votes_down_by_all_users
end
You shouldn't have to pass an object to itself as a variable as in ans.votes_up_by_all_users(ans). Objects always know about themselves.
Usually you can sort on a number of things by creating an array of these things and use that as your sort key:
#answers = hash_answers.sort_by { |k, v| [ v[:created_at], v[:count] }
This is dependent on having a sortable structure to start with. You're jamming two entirely different things into the same hash. A better approach might be:
hash_answers[ans.id] = {
:id => ans.id,
:count => voted_up_users.count -voted_down_users.count,
:created_at => ans.created_at
}
You can adjust the order of the elements in the array to sort in the correct order.

Ruby regex selecting multiple words at the same time

I have a hash that I am using regex on to select what key/value pairs I want. Here is the method I have written:
def extract_gender_race_totals(gender, race)
totals = #data.select {|k,v| k.to_s.match(/(#{gender})(#{race})/)}
temp = 0
totals.each {|key, value| temp += value}
temp
end
the hash looks like this:
#data = {
:number_of_african_male_senior_managers=>2,
:number_of_coloured_male_senior_managers=>0,
:number_of_indian_male_senior_managers=>0,
:number_of_white_male_senior_managers=>0,
:number_of_african_female_senior_managers=>0,
:number_of_coloured_female_senior_managers=>0,
:number_of_indian_female_senior_managers=>0,
:number_of_white_female_senior_managers=>0,
:number_of_african_male_middle_managers=>2,
:number_of_coloured_male_middle_managers=>0,
:number_of_indian_male_middle_managers=>0,
:number_of_white_male_middle_managers=>0,
:number_of_african_female_middle_managers=>0,
:number_of_coloured_female_middle_managers=>0,
:number_of_indian_female_middle_managers=>0,
:number_of_white_female_middle_managers=>0,
:number_of_african_male_junior_managers=>0,
:number_of_coloured_male_junior_managers=>0,
:number_of_indian_male_junior_managers=>0,
:number_of_white_male_junior_managers=>0,
:number_of_african_female_junior_managers=>0,
:number_of_coloured_female_junior_managers=>0,
:number_of_indian_female_junior_managers=>0,
:number_of_white_female_junior_managers=>0
}
but it's re-populated with data after a SQL Query.
I would like to make it so that the key must contain both the race and the gender in order for it to return something. Otherwise it must return 0. Is this right or is the regex syntax off?
It's returning 0 for all, which it shouldn't.
So the example would be
%td.total_cell= #ee_demographics_presenter.extract_gender_race_totals("male","african")
This would return 4, there are 4 African, male managers.
Try something like this.
def extract_gender_race_totals(gender, race)
#data.select{|k, v| k.to_s.match(/#{race}_#{gender}/)}.values.reduce(:+)
end
extract_gender_race_totals("male", "african")
# => 4
gmalete's answer gives an elegant solution, but here is just an explanation of why your regexp isn't quite right. If you corrected the regexp I think your approach would work, it just isn't as idiomatic Ruby.
/(#{gender})(#{race})/ won't match number_of_african_male_senior_managers for 2 reasons:
1) the race comes before the gender in the hash key and 2) there is an underscore in the hash key that needs to be in the regexp. e.g.
/(#{race})_(#{gender})/
would work, but the parentheses aren't needed so this can be simplified to
/#{race}_#{gender}/
Rather than having specific methods to query pieces of your keys (i.e. "gender_race"), you could make a general method to query any attribute in any order:
def extract_totals(*keywords)
keywords.inject(#data) { |memo, keyword| memo.select { |k, v| k.to_s =~ /_#{keyword}(?:_|\b)/ } }.values.reduce(:+)
end
Usage:
extract_totals("senior")
extract_totals("male", "african")
extract_totals("managers") # maybe you'll have _employees later...
# etc.
Not exactly what you asked for, but maybe it will help.

Resources