Reduce hash with key, value and index as block parameters - ruby

h = { "a" => 1, "b" => 2 }
Is there a way to reduce a hash and have the key, value and index as block parameters?
As a starting point I can iterate over a hash getting key, value and index:
h.each_with_index { |(k,v), i| puts [k,v,i].inspect }
# => ["a", 1, 0]
# => ["b", 2, 1]
However when I add reduce I seem to loose the ability to have the key and value as separate values and instead they are provided as a two element array:
h.each_with_index.reduce([]) { |memo, (kv,i)| puts [kv,i].inspect }
# => [["a", 1], 0]
# => [["b", 2], 1]
This is okay, I can in the block do kv[0] and kv[1], but I'd like something like this:
h.each_with_index.reduce([]) { |memo, (k,v), i| puts [k,v,i].inspect }
I'd like to do this without monkey-patching.

Maybe something like this?:
h.each_with_index.reduce([]) { |memo, ((k,v), i)| puts [k,v,i].inspect }
#=> ["a", 1, 0]
#=> ["b", 2, 1]
#=> nil
All you need is scoping: ((k,v), i).
Keeping in mind with reduce, we always have to return the object at the end of block. Which is kind of an extra overhead unless last operation isn't on the memo object which returns the object itself.Otherwise it won't return the desired result.
Same thing can be achieved with each_with_index chained with with_object like so:
h.each_with_index.with_object([]) { |((k,v), i), memo| memo << [k,v,i].inspect }
#=> ["a", 1, 0]
#=> ["b", 2, 1]
#=> []
See the array at last line of output? That's our memo object, which isn't same as reduce that we used above.

When in doubt what the block arguments are, create an instance of an Enumerator and call #next on it:
▶ h = {a: 1, b: 2}
#⇒ {:a=>1, :b=>2}
▶ enum = h.each.with_index.with_object([])
#⇒ #<Enumerator: ...>
▶ enum.next
#⇒ [[[:a, 1], 0], []]
The returned value consists of:
array of key and value, joined into:
array with an index, joined into:
array with an accumulator (for reduce it’d go in front, if reduce returned an enumerator when called without a block—credits to #Stefan for nitpicking.)
Hence, the proper parentheses for decomposing it would be:
# ⇓ ⇓ ⇓ ⇓
# [ [ [:a, 1], 0 ], [] ]
{ | ( (k, v), idx ), memo| ...

Enumerable#each_with_index yields two values into the block: the item and its index. When it is invoked for a Hash, the item is an array that contains two elements: the key and the associated value.
When you declare the block arguments |(k,v), i| you, in fact, deconstruct the first block argument (the item) into its two components: the key and the value. Without a block h.each_with_index produces an Enumerator that yields both arguments of the previously used block wrapped into an array.
This array is the second argument of Enumerator#reduce.
You can tell this by running:
irb> h.each_with_index.reduce([]) { |memo, j| p j }
[["a", 1], 0]
[["b", 2], 1]
Now, the answer to your question is easy: just deconstruct j and you get:
irb> h.each_with_index.reduce([]) { |memo, ((k,v), i)| puts [k,v,i].inspect }
["a", 1, 0]
["b", 2, 1]
Of course, you should memo << [k,v,i] or put the values in memo using other other rules and return memo to get your final desired result.

Related

Creating a Ruby Hash Map in a Functional Way

I have an array I want to turn into a hash map keyed by the item and with an array of indices as the value. For example
arr = ["a", "b", "c", "a"]
would become
hsh = {"a": [0,3], "b": [1], "c": [2]}
I would like to do this in a functional way (rather than a big old for loop), but am a little stuck
lst = arr.collect.with_index { |item, i| [item, i] }
produces
[["a", 0], ["b", 1], ["c", 2], ["a", 3]]
I then tried Hash[lst], but I don't get the array in the value and lose index 0
{"a"=>3, "b"=>1, "c"=>2}
How can I get my desired output in a functional way? I feel like it's something like
Hash[arr.collect.with_index { |item, i| [item, item[i] << i || [i] }]
But that doesn't yield anything.
Note: Trying to not do it this way
hsh = {}
arr.each.with_index do |item, index|
if hsh.has_key?(item)
hsh[item] << index
else
hsh[item] = [index]
end
end
hsh
Input
arr = ["a", "b", "c", "a"]
Code
p arr.map
.with_index
.group_by(&:first)
.transform_values { |arr| arr.map(&:last) }
Output
{"a"=>[0, 3], "b"=>[1], "c"=>[2]}
I would like to do this in a functional way (rather than a big old for loop), but am a little stuck
lst = arr.collect.with_index { |item, i| [item, i] }
produces
[["a", 0], ["b", 1], ["c", 2], ["a", 3]]
This is very close. The first thing I would do is change the inner arrays to hashes:
arr.collect.with_index { |item, i| { item => i }}
#=> [{ "a" => 0 }, { "b" => 1 }, { "c" => 2 }, { "a" => 3 }]
This is one step closer. Now, actually we want the indices in arrays:
arr.collect.with_index { |item, i| { item => [i] }}
#=> [{ "a" => [0] }, { "b" => [1] }, { "c" => [2] }, { "a" => [3] }]
This is even closer. Now, all we need to do is to merge those hashes into one single hash. There is a method for that, which is called Hash#merge. It takes an optional block for deconflicting duplicate keys, and all we need to do is concatenate the arrays:
arr.collect.with_index { |item, i| { item => [i] }}.inject({}) {|acc, h| acc.merge(h) {|_, a, b| a + b } }
#=> { "a" => [0, 3], "b" => [1], "c" => [2] }
And we're done!
How can I get my desired output in a functional way? I feel like it's something like
Hash[arr.collect.with_index { |item, i| [item, item[i] << i || [i] }]
But that doesn't yield anything.
Well, it has a SyntaxError, so obviously if it cannot even be parsed, then it cannot run, and if it doesn't even run, then it cannot possibly yield anything.
However, not that even if it worked, it would still violate your constraint that it should be done "in a functional way", because Array#<< mutates its receiver and is thus not functional.
arr.map.with_index.each_with_object({}){ |(a, i), h| h[a] ? h[a] << i : (h[a] = [i]) }
#=> {"a"=>[0, 3], "b"=>[1], "c"=>[2]}
arr.map.with_index => gives enumeration of each element with it's index
each_with_object => lets you reduce the enumeration on a provided object(represented by h in above)

Sum of same values in ruby hash

everybody.
I have hash for example
{-2=>"a", -1=>"c", 1=>"a", 3=>"a", 49=>"a", -43=>"ab", 5=>"ab"}
There can be equal values. My task is to sum keys where values are equal. Result:
{51=>"a", -1=>"c", -38=>"ab"}
How can I do this?
hash.group_by{|key,val| val}
Gives awful result.
hash = {-2=>"a", -1=>"c", 1=>"a", 3=>"a", 49=>"a", -43=>"ab", 5=>"ab"}
hash.reduce({}) do |memo, (k,v)|
memo[v] ||= 0
memo[v] += k
memo
end.invert
# => {51=>"a", -1=>"c", -38=>"ab"}
reduce - lets you build up a new value by iterating over the values of a collection, in this case hash. See the docs for more.
invert - swaps the keys and values of a hash. See the docs for more.
Other ways to do this:
hash.reduce(Hash.new(0)) { |memo, (k,v)| memo[v] += k; memo }.invert
h = {-2=>"a", -1=>"c", 1=>"a", 3=>"a", 49=>"a", -43=>"ab", 5=>"ab"}
then
h.group_by(&:last).each_with_object({}) { |(k,v),h| h[v.map(&:first).sum] = k }
#=> {51=>"a", -1=>"c", -38=>"ab"}
but that would be crazy as it relies on the sums being unique. (Recall that hashes have unique keys.) Suppose
h = {-54=>"a", -1=>"c", 1=>"a", 3=>"a", 49=>"a", -43=>"ab", 5=>"ab"}
then
h.group_by(&:last).each_with_object({}) { |(k,v),h| h[v.map(&:first).sum] = k }
#=> {-1=>"c", -38=>"ab"}
as -1=>"a" is overwritten by -1=<"c". I doubt that this is wanted.
It would be better to save the contents of h in an array:
a = [[-2, "a"], [-1, "c"], [-1, "a"], [49, "a"], [-43, "ab"], [5, "ab"]]
(as it permits duplicate values of the integers--here -1) and then compute
a.group_by(&:last).each_with_object({}) { |(e,ar),h| h[e] = ar.map(&:first).sum }
#=> {"a"=>46, "c"=>-1, "ab"=>-38}
Note that (for the original value of h)
h.group_by(&:last)
#=> {"a"=>[[-2, "a"], [1, "a"], [3, "a"], [49, "a"]],
# "c"=>[[-1, "c"]], "ab"=>[[-43, "ab"], [5, "ab"]]}
and v.map(&:first).sum could be replaced with
v.reduce(0) { |t,(n,_)| t+n }

Inverting a hash value (that's an array) into new individual keys

I have the following:
lumpy_hash = { 1 => ["A", "B"] }
then if I invoke Hash#invert on this hash, I'd like to get:
lumpy_hash = {"A" => 1, "B" => 1}
I don't get that from using Hash#invert. Any ideas on doing this? I'm not sure if I should try Hash#map or Hash#invert.
There are many ways to do this. Here is one:
Hash[lumpy_hash.map { |k,v| v.product([k]) }.first]
#=> {"A"=>1, "B"=>1}
I don't think the method Hash#invert is useful here.
The steps:
enum = lumpy_hash.map
#=> #<Enumerator: {1=>["A", "B"]}:map>
k,v = enum.next
#=> [1, ["A", "B"]]
k #=> 1
v #=> ["A", "B"]
a = v.product([k])
#=> ["A", "B"].product([1])
#=> [["A", 1], ["B", 1]]
Hash[a]
#=> {"A"=>1, "B"=>1}
Here's another way that makes use of a hash's default value. This one is rather interesting:
key,value = lumpy_hash.to_a.first
#=> [1, ["A","B"]]
Hash.new { |h,k| h[k]=key }.tap { |h| h.values_at(*value) }
#=> {"A"=>1,"B"=>1}
Object#tap passes an empty hash to its block, assigning it to the block variable h. The block returns h after adding three key-value pairs, each having a value equal to the hash's default value. It adds the pairs merely by computing the values of keys the hash doesn't have!
Here's another, more pedestrian, method:
lumpy_hash.flat_map{|k,vs| vs.map{|v| {v => k}}}.reduce(&:merge)
=> {"A"=>1, "B"=>1}

Swap hash keys with values and convert keys to symbols in Ruby?

This is the input hash:
p Score.periods #{"q1"=>0, "q2"=>1, "q3"=>2, "q4"=>3, "h1"=>4, "h2"=>5}
This is my current code to exchange the keys with the values, while converting the keys to symbols:
periods = Score.periods.inject({}) do |hsh,(k,v)|
hsh[v] = k.to_sym
hsh
end
Here is the result:
p periods #{0=>:q1, 1=>:q2, 2=>:q3, 3=>:q4, 4=>:h1, 5=>:h2}
It just seems like my code is clunky and it shouldn't take 4 lines to do what I'm doing here. Is there a cleaner way to write this?
You can do this:
Hash[periods.values.zip(periods.keys.map(&:to_sym))]
Or if you're using a version of Ruby where to_h is available for arrays, you can do this:
periods.values.zip(periods.keys.map(&:to_sym)).to_h
What the two examples above do is make arrays of the keys and values of the original hash. Note that the string keys of the hash are mapped to symbols by passing to_sym to map as a Proc:
periods.keys.map(&:to_sym)
# => [:q1, :q2, :q3, :q4, :h1, :h2]
periods.values
# => [0, 1, 2, 3, 4, 5]
Then it zips them up into an array of [value, key] pairs, where each corresponding elements of values is matched with its corresponding key in keys:
periods.values.zip(periods.keys.map(&:to_sym))
# => [[0, :q1], [1, :q2], [2, :q3], [3, :q4], [4, :h1], [5, :h2]]
Then that array can be converted back into a hash using Hash[array] or array.to_h.
The simplest way is:
data = {"q1"=>0, "q2"=>1, "q3"=>2, "q4"=>3, "h1"=>4, "h2"=>5}
Hash[data.invert.collect { |k, v| [ k, v.to_sym ] }]
The Hash[] method converts an array of key/value pairs into an actual Hash. Quite handy for situations like this.
If you're using Ruby on Rails this could be even easier:
data.symbolize_keys.invert
h = {"q1"=>0, "q2"=>1, "q3"=>2, "q4"=>3, "h1"=>4, "h2"=>5}
h.each_with_object({}) { |(k,v),g| g[v] = k.to_sym }
#=> {0=>:q1, 1=>:q2, 2=>:q3, 3=>:q4, 4=>:h1, 5=>:h2}
The steps are as follows (for the benefit of Ruby newbies).
enum = h.each_with_object({})
#=> #<Enumerator: {0=>"q1", 1=>"q2", 2=>"q3", 3=>"q4",
# 4=>"h1", 5=>"h2"}:each_with_object({})>
The elements that will be generated by the enumerator and passed to the block can be seen by converting the enumerator to an array, using Enumerable#entries or Enumerable#to_a.
enum.entries
#=> [[["q1", 0], {}], [["q2", 1], {}], [["q3", 2], {}],
# [["q4", 3], {}], [["h1", 4], {}], [["h2", 5], {}]]
Continuing,
enum.each { |(k,v),g| g[v] = k.to_sym }
#=> {0=>:q1, 1=>:q2, 2=>:q3, 3=>:q4, 4=>:h1, 5=>:h2}
In the last step, Enumerator#each passes the first element generated by enum to the block and assigns the three block variables. Consider the first element of enum that is passed to the block and the associated calculation of values for the three block variables. (I must first execute enum.rewind to reinitialize enum, as each above took the enumerator to its end. See Enumerator#rewind).
(k, v), g = enum.next
#=> [["q1", 0], {}]
k #=> "q1"
v #=> 0
g #=> {}
See Enumerator#next. The block calculation is therefore
g[v] = k.to_sym
#=> :q1
Hence,
g #=> {0=>:q1}
The next element of enum is passed to the block and similar calculations are performed.
(k, v), g = enum.next
#=> [["q2", 1], {0=>:q1}]
k #=> "q2"
v #=> 1
g #=> {0=>:q1}
g[v] = k.to_sym
#=> :q2
g #=> {0=>:q1, 1=>:q2}
The remaining calculations are similar.

Remove element from array by type

I have the following array:
["--",1,2,3,4]
How can I remove elements from the array by element type, ie. remove all non-integer values from the array?
I'd do :-
ary = ["--",1,2,3,4]
ary = ary.grep(Integer)
ary # => [1, 2, 3, 4]
Note :- If you don't want to mutate the original array use new_ary instead of ary. Like
new_ary = ary.grep(Integer)
You can use delete_if to remove items from the list, however this modifies the list.
a = ["--", 1, 2, 3, 4]
a.delete_if { |n| !n.kind_of?(Fixnum) }
p a
You can select items out of the list maintaining the original list by using select
a = ["--", 1, 2, 3, 4]
b = a.select { |n| n.kind_of?(Fixnum) }
p b
p a
This solution addresses the title, rather than the example, and permits the selection of elements by class, as well as the rejection of elements by class.
Code
good_classes and bad_classes are arrays of classes.
def filter_select(arr, *good_classes)
arr.select { |e| good_classes.include? e.class }
end
def filter_reject(arr, *bad_classes)
arr.reject { |e| bad_classes.include? e.class }
end
Examples
arr = [1, :a, {b: 3}, "cat", [4,5], true, 3..4, false]
filter_select(arr, Fixnum, Hash, TrueClass, Range)
#=> [1, {:b=>3}, true, 3..4]
filter_reject(arr, Fixnum, Hash, String, Array)
#=> [:a, true, 3..4, false]
I'd do
new_array = ary.reject {|x| x.is_a?(String)}

Resources