group_by one attribute and map another attribute? - ruby

I have a list of objects with attributes A and B. Is there a one-liner way to store B values in arrays grouped by A values, in one loop? (not group_by, and then map) I'd have, as a result, an array of arrays.
result[a1] = [b1, b4, b5]
Edit : apparently i wasn't clear enough, sorry.
I have a list of objects like this :
class MyObject
attr_accessor :attrA, :attrB
end
and i'd like a method that takes a list of MyObjects and returns all the attrB values grouped by attrA values.
Kristjan answer is good, and if there are no one liner doing this, it will be the accepted answer

Is a two-liner ok? Set up a results hash with a default empty array value, then loop through your items concatenating the value you want into the group you want.
list = [
['a', 1],
['b', 2],
['a', 3],
['b', 4]
]
result = Hash.new { |h, k| h[k] = [] }
list.each do |item|
result[item.first] << item.last
end
puts result.inspect
# {"a"=>[1, 3], "b"=>[2, 4]}

Related

Ruby Hash initialised with each_with_object behaving weirdly

Initializing Ruby Hash like:
keys = [0, 1, 2]
hash = Hash[keys.each_with_object([]).to_a]
is behaving weirdly when trying to insert a value into a key.
hash[0].push('a')
# will result into following hash:
=> {0=>["a"], 1=>["a"], 2=>["a"]}
I am just trying to insert into one key, but it's updating the value of all keys.
Yes, that each_with_object is super-weird in itself. That's not how it should be used. And the problem arises precisely because you mis-use it.
keys.each_with_object([]).to_a
# => [[0, []], [1, []], [2, []]]
You see, even though it looks like these arrays are separate, it's actually the same array in all three cases. That's why if you push an element into one, it appears in all others.
Here's a better way:
h = keys.each_with_object({}) {|key, h| h[key] = []}
# => {0=>[], 1=>[], 2=>[]}
Or, say
h = keys.zip(Array.new(keys.size) { [] }).to_h
Or a number of other ways.
If you don't care about hash having this exact set of keys and simply want all keys to have empty array as default value, that's possible too.
h = Hash.new { |hash, key| hash[key] = [] }
All your keys reference the same array.
A simplified version that explains the problem:
a = []
b = a
a.push('something')
puts a #=> ['something']
puts b #=> ['something']
Even though you have two variables (a and b) there is only one Array Object. So any changes to the array referenced by variable a will change the array referenced by variable b as well. Because it is the same object.
The long version of what you are trying to achieve would be:
keys = [1, 2, 3]
hash = {}
keys.each do |key|
hash[key] = []
end
And a shorter version:
[1, 2 ,3].each_with_object({}) do |key, accu|
accu[key] = []
end

Iterating over an array of arrays

def compute(ary)
return nil unless ary
ary.map { |a, b| !b.nil? ? a + b : a }
end
compute([1,2],[3,4])
Can someone please explain to me how compute adds the inner array's values?
To me it seems that calling map on that array of arrays would add the two arrays together, not the inner elements of each array.
map basically iterates over the elements of the object:
foo = [
['a', 'b'],
['c', 'd']
]
foo.map{ |ary| puts ary.join(',') }
# >> a,b
# >> c,d
In this example it's passing each sub-array, which is assigned to ary.
Looking at it a bit differently:
foo.map{ |ary| puts "ary is a #{ary.class}" }
# >> ary is a Array
# >> ary is a Array
Because Ruby lets us assign multiple values at once, that could have been written:
foo.map{ |item1, item2| puts "item1: #{ item1 }, item2: #{ item2 }" }
# >> item1: a, item2: b
# >> item1: c, item2: d
If map is iterating over an array of hashes, each iteration yields a sub-hash to the block:
foo = [
{'a' => 1},
{'b' => 2}
]
foo.map{ |elem| puts "elem is a #{ elem.class }" }
# >> elem is a Hash
# >> elem is a Hash
If map is iterating over a hash, each iteration yields the key/value pair to the block:
foo = {
'a' => 1,
'b' => 2
}
foo.map{ |k, v| puts "k: #{k}, v: #{v}" }
# >> k: a, v: 1
# >> k: b, v: 2
However, if you only give the block a single parameter, Ruby will assign both the key and value to the variable so you'll see it as an array:
foo.map{ |ary| puts "ary is a #{ary.class}" }
# >> ary is a Array
# >> ary is a Array
So, you have to be aware of multiple things that are happening as you iterate over the container, and as Ruby passes the values into map's block.
Beyond all that, it's important to remember that map is going to return a value, or values, for each thing passed in. map, AKA collect, is used to transform the values. It shouldn't be used as a replacement for each, which only iterates. In all the examples above I didn't really show map used correctly because I was trying to show what happens to the elements passed in. Typically we'd do something like:
foo = [['a', 'b'], ['c', 'd']]
foo.map{ |ary| ary.join(',') }
# => ["a,b", "c,d"]
Or:
bar = [[1,2], [3,4]]
bar.collect{ |i, j| i * j }
# => [2, 12]
There's also map! which changes the object being iterated, rather than returns the values. I'd recommend avoiding map! until you're well aware of why it'd be useful to you, because it seems to confuse people no end unless they understand how variables are passed and how Arrays and Hashes work.
The best thing is to play with map in IRB. You'll be able to see what's happening more easily.
I think I figured this out myself.
map selects the first array of the array-of-arrays and pipes it into the block. The variables a and b therefore refer to the first array's inner elements, rather than the first array and the second array in the array-of-arrays.

How to display dynamic case statement in Ruby

How would I write a case statement that would list all elements in an array, allow the user to pick one, and do processing on that element?
I have an array:
array = [ 'a', 'b', 'c', 'd' ]
Ultimately I'd like it to behave like this:
Choices:
1) a
2) b
3) c
4) d
Choice =>
After the user picks 3, I would then do processing based off the choice of the user. I can do it in bash pretty easily.
Ruby has no built-in menu stuff like shell scripting languages do. When doing menus, I favor constructing a hash of possible options and operating on that:
def array_to_menu_hash arr
Hash[arr.each_with_index.map { |e, i| [i+1, e] }]
end
def print_menu menu_hash
puts 'Choices:'
menu_hash.each { |k,v| puts "#{k}) #{v}" }
puts
end
def get_user_menu_choice menu_hash
print 'Choice => '
number = STDIN.gets.strip.to_i
menu_hash.fetch(number, nil)
end
def show_menu menu_hash
print_menu menu_hash
get_user_menu_choice menu_hash
end
def user_menu_choice choice_array
until choice = show_menu(array_to_menu_hash(choice_array)); end
choice
end
array = %w{a b c d}
choice = user_menu_choice(array)
puts "User choice was #{choice}"
The magic happens in array_to_menu_hash:
The [] method of Hash converts an array with the form [ [1, 2], [3, 4] ] to a hash {1 => 2, 3 => 4}. To get this array, we first call each_with_index on the original menu choice array. This returns an Enumerator that emits [element, index_number] when iterated. There are two problems with this Enumerator: the first is that Hash[] needs an array, not an Enumerator. The second is that the arrays emitted by the Enumerator have the elements in the wrong order (we need [index_number, element]). Both of these problems are solved with #map. This converts the Enumerator from each_with_index into an array of arrays, and the block given to it allows us to alter the result. In this case, we are adding one to the zero-based index and reversing the order of the sub-arrays.

How to remove elements of array in place returning the removed elements

I have an array arr. I want to destructively remove elements from arr based on a condition, returning the removed elements.
arr = [1,2,3]
arr.some_method{|a| a > 1} #=> [2, 3]
arr #=> [1]
My first try was reject!:
arr = [1,2,3]
arr.reject!{|a| a > 1}
but the returning blocks and arr's value are both [1].
I could write a custom function, but I think there is an explicit method for this. What would that be?
Update after the question was answered:
partition method turns out to be useful for implementing this behavior for hash as well. How can I remove elements of a hash, returning the removed elements and the modified hash?
hash = {:x => 1, :y => 2, :z => 3}
comp_hash, hash = hash.partition{|k,v| v > 1}.map{|a| Hash[a]}
comp_hash #=> {:y=>2, :z=>3}
hash #=> {:x=>1}
I'd use partition here. It doesn't modify self inplace, but returns two new arrays. By assigning the second array to arr again, it gets the results you want:
comp_arr, arr = arr.partition { |a| a > 1 }
See the documentation of partition.
All methods with a trailing bang ! modify the receiver and it seems to be a convention that these methods return the resulting object because the non-bang do so.
What you can to do though is something like this:
b = (arr.dup - arr.reject!{|a| a>1 })
b # => [2,3]
arr #=> [1]
Here is a link to a ruby styleguide which has a section on nameing - although its rather short
To remove (in place) elements of array returning the removed elements one could use delete method, as per Array class documentation:
a = [ "a", "b", "b", "b", "c" ]
a.delete("b") #=> "b"
a #=> ["a", "c"]
a.delete("z") #=> nil
a.delete("z") { "not found" } #=> "not found"
It accepts block so custom behavior could be added, as needed

Sort items in a nested hash by their values

I'm being sent a nested hash that needs to be sorted by its values. For example:
#foo = {"a"=>{"z"=>5, "y"=>3, "x"=>88}, "b"=>{"a"=>2, "d"=>-5}}
When running the following:
#foo["a"].sort{|a,b| a[1]<=>b[1]}
I get:
[["y", 3], ["z", 5], ["x", 88]]
This is great, it's exactly what I want. The problem is I'm not always going to know what all the keys are that are being sent to me so I need some sort of loop. I tried to do the following:
#foo.each do |e|
e.sort{|a,b| a[1]<=>b[1]}
end
This to me makes sense since if I manually call #foo.first[0] I get
"a"
and #foo.first[1] returns
{"z"=>5, "y"=>3, "x"=>8}
but for some reason this isn't sorting properly (e.g. at all). I assume this is because the each is calling sort on the entire hash object rather than on "a"'s values. How do I access the values of the nested hash without knowing what it's key is?
You might want to loop over the hash like this:
#foo.each do |key, value|
#foo[key] = value.sort{ |a,b| a[1]<=>b[1] }
end
#foo = {"a"=>{"z"=>5, "y"=>3, "x"=>88}, "b"=>{"a"=>2, "d"=>-5}}
#bar = Hash[ #foo.map{ |key,values| [ key, values.sort_by(&:last) ] } ]
Or, via a less-tricky path:
#bar = {}
#foo.each do |key,values|
#bar[key] = values.sort_by{ |key,value| value }
end
In both cases #bar turns out to be:
p #bar
#=> {
#=> "a"=>[["y", 3], ["z", 5], ["x", 88]],
#=> "b"=>[["d", -5], ["a", 2]]
#=> }
My coworker came up with a slightly more flexible solution that will recursively sort an array of any depth:
def deep_sort_by(&block)
Hash[self.map do |key, value|
[if key.respond_to? :deep_sort_by
key.deep_sort_by(&block)
else
key
end,
if value.respond_to? :deep_sort_by
value.deep_sort_by(&block)
else
value
end]
end.sort_by(&block)]
end
You can inject it into all hashes and then just call it like this:
myMap.deep_sort_by { |obj| obj }
The code would be similar for an array. We published it as a gem for others to use, see blog post for additional details.
Disclaimer: I work for this company.
in your example e is an temporary array containing a [key,value] pair. In this case, the character key and the nested hash. So e.sort{|a,b|...} is going to try to compare the character to the hash, and fails with a runtime error. I think you probably meant to type e[1].sort{...}. But even that is not going to work correctly, because you don't store the sorted hash anywhere: #foo.each returns the original #foo and leaves it unchanged.
The better solution is the one suggested by #Pan Thomakos:
#foo.each do |key, value|
#foo[key] = value.sort{ |a,b| a[1]<=>b[1] }
end

Resources