Given a set of possible values, and a hash of arbitrary number of values, how can I replace every nil value with every possible combination of the possible values?
For example:
values = %w[a b c]
hash = { x:1, y:2, z:nil }
fill_wildcards( hash, values )
#=> [{ x:1, y:2, z:'a' },
#=> { x:1, y:2, z:'b' },
#=> { x:1, y:2, z:'c' }]
hash = { x:1, y:nil, z:nil }
fill_wildcards( hash, values )
#=> [{ x:1, y:'a', z:'a' },
#=> { x:1, y:'a', z:'b' },
#=> { x:1, y:'a', z:'c' },
#=> { x:1, y:'b', z:'a' },
#=> { x:1, y:'b', z:'b' },
#=> { x:1, y:'b', z:'c' },
#=> { x:1, y:'c', z:'a' },
#=> { x:1, y:'c', z:'b' },
#=> { x:1, y:'c', z:'c' }]
I can find the keys that need to be replaced:
wildkeys = hash.select{ |k,v| v.nil? }.map(&:first)
#=> [:y, :z]
And thus I can find all the permutations of values needed:
wildvalues = values.repeated_permutation( wildkeys.length ).to_a
#=> [["a", "a"], ["a", "b"], ["a", "c"], ["b", "a"],
#=> ["b", "b"], ["b", "c"], ["c", "a"], ["c", "b"], ["c", "c"]]
But I can't think of a simple way to merge these two into the original.
Might be something like this:
rest = hash.reject { |k,v| v.nil? }.to_a
wildvalues.map { |wv| Hash[rest + wildkeys.zip(wv)] }
or even
wildvalues.map { |wv| hash.merge(Hash[wildkeys.zip(wv)]) }
def fill_wildcards( hsh, values )
values.repeated_permutation(hsh.values.count(nil)).to_a.map {|combo| hsh.each_with_object(hsh.dup) {|(k,v),hsh| hsh[k] = combo.shift unless v } }
end
Another way:
Code
def doit(hash, values)
a = hash.map { |k,v| [k].product(v ? [v] : values) }
a.shift.product(*a).map(&:to_h)
end
Demo
values = %w[a b c]
hash = { x:1, y:2, z:nil }
p doit(hash, values)
#=> [{:x=>1, :y=>2, :z=>"a"},
# {:x=>1, :y=>2, :z=>"b"},
# {:x=>1, :y=>2, :z=>"c"}]
hash = { x:1, y:nil, z:nil }
p doit(hash, values)
#=> [{:x=>1, :y=>"a", :z=>"a"},
# {:x=>1, :y=>"a", :z=>"b"},
# {:x=>1, :y=>"a", :z=>"c"},
# {:x=>1, :y=>"b", :z=>"a"},
# {:x=>1, :y=>"b", :z=>"b"},
# {:x=>1, :y=>"b", :z=>"c"},
# {:x=>1, :y=>"c", :z=>"a"},
# {:x=>1, :y=>"c", :z=>"b"},
# {:x=>1, :y=>"c", :z=>"c"}]
hash = { x:nil, y:nil, z:nil }
p doit(hash, values).size #=> 27
Related
Write a method that groups the above hash into 2 groups of 'even' and 'odd' length using 'inject'.
input - "['abc','def','1234','234','abcd','x','mnop','5','zZzZ']"
My code listed below already works. But I want to know better way to do it using default value for hash's key. I meant to say something like below -
h=Hash.new { |hash, key| hash[key] = []}
Solution :
class Array
def group_even_odd
key_hash = group_by(&:length)
key_array = %w(even odd)
key_hash.each_with_object('odd' => [], 'even' => []) do |(key, value), even_odd_hash|
even_odd_hash[key_array[key % 2]].push(value)
even_odd_hash
end
end
end
if ARGV.empty?
puts 'Please provide an input'
else
input = ARGV[0].scan(/\w+/).map(&:to_s)
puts input.group_even_odd
end
Expected and actual are same, code is working.
Expected result -
{"odd"=>[["abc", "def", "234"], ["x", "5"]], "even"=>[["1234", "abcd", "mnop", "zZzZ"]]}
One possible option, given
ary = ["abc", "def", "1234", "234", "abcd", "x", "mnop", "5", "zZzZ"]
First group by even odd, then group by size:
ary.group_by { |e| e.size.even? ? 'even' : 'odd' }
.transform_values { |v| v.group_by(&:size).values }
#= {"odd"=>[["abc", "def", "234"], ["x", "5"]], "even"=>[["1234", "abcd", "mnop", "zZzZ"]]}
First step, to explain:
ary.group_by { |e| e.size.even? ? 'even' : 'odd' }
#=> {"odd"=>["abc", "def", "234", "x", "5"], "even"=>["1234", "abcd", "mnop", "zZzZ"]}
Then Hash#transform_values grouping each by size.
The following does not meet the requirement that inject (aka reduce) be used, but it is how I would do it.
arr = ['abc', 'def', '1234', '234', 'abcd', 'x', 'mnop', '5', 'zZzZ']
odd, even = arr.each_with_object(Hash.new { |h,k| h[k]=[] }) do |s,h|
h[s.size] << s
end.
values.
partition { |a| a.first.size.odd? }
#=> [[["abc", "def", "234"], ["x", "5"]],
# [["1234", "abcd", "mnop", "zZzZ"]]]
{ "odd"=>odd, "even"=>even }
#=> {"odd"=>[["abc", "def", "234"], ["x", "5"]],
# "even"=>[["1234", "abcd", "mnop", "zZzZ"]]}
The steps are as follows.
h = arr.each_with_object(Hash.new {|h,k| h[k]=[]}) do |s,h|
h[s.size] << s
end
#=> {3=>["abc", "def", "234"], 4=>["1234", "abcd", "mnop", "zZzZ"],
# 1=>["x", "5"]}
a = h.values
#=> [["abc", "def", "234"], ["1234", "abcd", "mnop", "zZzZ"],
# ["x", "5"]]
odd, even = a.partition { |a| a.first.size.odd? }
#=> [[["abc", "def", "234"], ["x", "5"]],
# [["1234", "abcd", "mnop", "zZzZ"]]]
{ "odd"=>odd, "even"=>even }
#=> {"odd"=>[["abc", "def", "234"], ["x", "5"]],
# "even"=>[["1234", "abcd", "mnop", "zZzZ"]]}
If one insists on fitting a square peg into a round hold (using inject/reduce), I suppose that could be done as follows.
arr.reduce({ "odd"=>[], "even"=>[] }) do |g,s|
oe = s.size.odd? ? "odd" : "even"
i = g[oe].find_index { |a| a.any? && a.first.size == s.size }
case i.nil?
when true then g[oe] << [s]
else g[oe][i] << s
end
g
end
#=> {"odd"=>[["abc", "def", "234"], ["x", "5"]],
# "even"=>[["1234", "abcd", "mnop", "zZzZ"]]}
How to merge array of hash based on the same keys in ruby?
example :
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
How to get result like this?
a = [{:a=>[1, 10]},{:b=>8},{:c=>[7, 2]}]
Try
a.flat_map(&:entries)
.group_by(&:first)
.map{|k,v| Hash[k, v.map(&:last)]}
Another alternative:
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
p a.each_with_object({}) { |h, o| h.each { |k,v| (o[k] ||= []) << v } }
# => {:a=>[1, 10], :b=>[8], :c=>[7, 2]}
It also works when the Hashes have multiple key/value combinations, e.g:
b = [{:a=>1, :b=>5, :x=>10},{:a=>10, :y=>2},{:b=>8},{:c=>7},{:c=>2}]
p b.each_with_object({}) { |h, o| h.each { |k,v| (o[k] ||= []) << v } }
# => {:a=>[1, 10], :b=>[5, 8], :x=>[10], :y=>[2], :c=>[7, 2]}
Minor addition to answer by Arie Shaw to match required answer:
a.flat_map(&:entries)
.group_by(&:first)
.map{|k,v| Hash[k, v.size.eql?(1) ? v.last.last : v.map(&:last) ]}
#=> [{:a=>[1, 10]}, {:b=>8}, {:c=>[7, 2]}]
I'd do :
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
merged_hash = a.each_with_object({}) do |item,hsh|
k,v = item.shift
hsh[k] = hsh.has_key?(k) ? [ *Array( v ), hsh[k] ] : v
end
merged_hash.map { |k,v| { k => v } }
# => [{:a=>[10, 1]}, {:b=>8}, {:c=>[2, 7]}]
update
A better taste :
a = [{:a=>1},{:a=>10},{:b=>8},{:c=>7},{:c=>2}]
merged_hash = a.each_with_object({}) do |item,hsh|
k,v = item.shift
(hsh[k] ||= []) << v
end
merged_hash.map { |k,v| { k => v } }
# => [{:a=>[10, 1]}, {:b=>8}, {:c=>[2, 7]}]
let's have this hash:
hash = {"a" => 1, "b" => {"c" => 3}}
hash.get_all_keys
=> ["a", "b", "c"]
how can i get all keys since hash.keys returns just ["a", "b"]
This will give you an array of all the keys for any level of nesting.
def get_em(h)
h.each_with_object([]) do |(k,v),keys|
keys << k
keys.concat(get_em(v)) if v.is_a? Hash
end
end
hash = {"a" => 1, "b" => {"c" => {"d" => 3}}}
get_em(hash) # => ["a", "b", "c", "d"]
I find grep useful here:
def get_keys(hash)
( hash.keys + hash.values.grep(Hash){|sub_hash| get_keys(sub_hash) } ).flatten
end
p get_keys my_nested_hash #=> ["a", "b", "c"]
I like the solution as it is short, yet it reads very nicely.
Version that keeps the hierarchy of the keys
Works with arrays
Works with nested hashes
keys_only.rb
# one-liner
def keys_only(h); h.map { |k, v| v = v.first if v.is_a?(Array); v.is_a?(Hash) ? [k, keys_only(v)] : k }; end
# nicer
def keys_only(h)
h.map do |k, v|
v = v.first if v.is_a?(Array);
if v.is_a?(Hash)
[k, keys_only(v)]
else
k
end
end
end
hash = { a: 1, b: { c: { d: 3 } }, e: [{ f: 3 }, { f: 5 }] }
keys_only(hash)
# => [:a, [:b, [[:c, [:d]]]], [:e, [:f]]]
P.S.: Yes, it looks like a lexer :D
Bonus: Print the keys in a nice nested list
# one-liner
def print_keys(a, n = 0); a.each { |el| el.is_a?(Array) ? el[1] && el[1].class == Array ? print_keys(el, n) : print_keys(el, n + 1) : (puts " " * n + "- #{el}") }; nil; end
# nicer
def print_keys(a, n = 0)
a.each do |el|
if el.is_a?(Array)
if el[1] && el[1].class == Array
print_keys(el, n)
else
print_keys(el, n + 1)
end
else
puts " " * n + "- #{el}"
end
end
nil
end
> print_keys(keys_only(hash))
- a
- b
- c
- d
- e
- f
def get_all_keys(hash)
hash.map do |k, v|
Hash === v ? [k, get_all_keys(v)] : [k]
end.flatten
end
Please take a look of following code:
hash = {"a" => 1, "b" => {"c" => 3}}
keys = hash.keys + hash.select{|_,value|value.is_a?(Hash)}
.map{|_,value| value.keys}.flatten
p keys
result:
["a", "b", "c"]
New solution, considering #Bala's comments.
class Hash
def recursive_keys
if any?{|_,value| value.is_a?(Hash)}
keys + select{|_,value|value.is_a?(Hash)}
.map{|_,value| value.recursive_keys}.flatten
else
keys
end
end
end
hash = {"a" => 1, "b" => {"c" => {"d" => 3}}, "e" => {"f" => 3}}
p hash.recursive_keys
result:
["a", "b", "e", "c", "d", "f"]
Also deal with nested arrays that include hashes
def all_keys(items)
case items
when Hash then items.keys + items.values.flat_map { |v| all_keys(v) }
when Array then items.flat_map { |i| all_keys(i) }
else []
end
end
class Hash
def get_all_keys
[].tap do |result|
result << keys
values.select { |v| v.respond_to?(:get_all_keys) }.each do |value|
result << value.get_all_keys
end
end.flatten
end
end
hash = {"a" => 1, "b" => {"c" => 3}}
puts hash.get_all_keys.inspect # => ["a", "b", "c"]
Here is another approach :
def get_all_keys(h)
h.each_with_object([]){|(k,v),a| v.is_a?(Hash) ? a.push(k,*get_all_keys(v)) : a << k }
end
hash = {"a" => 1, "b" => {"c" => {"d" => 3}}}
p get_all_keys(hash)
# >> ["a", "b", "c", "d"]
I'm sure there is a more elegant solution, but this option works:
blah = {"a" => 1, "b" => {"c" => 3}}
results = []
blah.each do |k,v|
if v.is_a? Hash
results << k
v.each_key {|key| results << key}
else
results << k
end
end
puts results
hash.keys is the simplest one I have seen to return an array of the key values in a hash.
I have
a = ["a", "d", "c", "b", "b", "c", "c"]
and need to print something like (sorted descending by number of occurrences):
c:3
b:2
I understand first part (finding NON-unique) is:
b = a.select{ |e| a.count(e) > 1 }
=> ["c", "b", "b", "c", "c"]
or
puts b.select{|e, c| [e, a.count(e)] }.uniq
c
b
How to output each non-unique with number of occurrences sorted backwards?
puts a.uniq.
map { | e | [a.count(e), e] }.
select { | c, _ | c > 1 }.
sort.reverse.
map { | c, e | "#{e}:#{c}" }
The group_by method is used for this often:
a.group_by{ |i| i }
{
"a" => [
[0] "a"
],
"d" => [
[0] "d"
],
"c" => [
[0] "c",
[1] "c",
[2] "c"
],
"b" => [
[0] "b",
[1] "b"
]
}
I like:
a.group_by{ |i| i }.each_with_object({}) { |(k,v), h| h[k] = v.size }
{
"a" => 1,
"d" => 1,
"c" => 3,
"b" => 2
}
Or:
Hash[a.group_by{ |i| i }.map{ |k,v| [k, v.size] }]
{
"a" => 1,
"d" => 1,
"c" => 3,
"b" => 2
}
One of those might scratch your itch. From there you can reduce the result using a little test:
Hash[a.group_by{ |i| i }.map{ |k,v| v.size > 1 && [k, v.size] }]
{
"c" => 3,
"b" => 2
}
If you just want to print the information use:
puts a.group_by{ |i| i }.map{ |k,v| "#{k}: #{v.size}" }
a: 1
d: 1
c: 3
b: 2
From Ruby 2.7, you can utilise Enumerable#tally and numbered block arguments:
a = ["a", "d", "c", "b", "b", "c", "c"]
puts a.tally.filter { _2 > 1 }.sort_by { -_2 }.map &:first
Here, Enumerable#tally returns a hash like { 'a' => 1, 'b' => 2, ... }, which you then have to filter and sort. After sorting, the hash would've collapsed to a nested array, e.g. [['b', 2], ...]. The last step is to take the first argument of each array element, using &:first.
How about:
a.sort.chunk{|x| a.count(x)}.sort.reverse.each do |n, v|
puts "#{v[0]}:#{n}" if n > 1
end
I personally like this solution:
a.inject({}) {|hash, val| hash[val] ||= 0; hash[val] += 1; hash}.
reject{|key, value| value == 1}.sort.reverse.
each_pair{|k,v| puts("#{k}:#{v}")}
a.reduce(Hash.new(0)) { |memo,x| memo[x] += 1; memo } # Frequency count.
.select { |_,count| count > 1 } # Choose non-unique items.
.sort_by { |x| -x[1] } # Sort by number of occurrences descending.
# => [["c", 3], ["b", 2]]
Also:
a.group_by{|x|x}.map{|k,v|[k,v.size]}.select{|x|x[1]>1}.sort_by{|x|-x[1]}
# => [["c", 3], ["b", 2]]
This will give you a hash with element => occurrences:
b.reduce(Hash.new(0)) do |hash, element|
hash[element] += 1
hash
end
puts a.uniq.
map { |e| a.count(e) > 1 ? [e, a.count(e)] : nil }.compact.
sort { |a, b| b.last <=> a.last }
I have some array
>> a = ["a..c", "0..2"]
=> ["a..c", "0..2"]
I need convert this array to another array
>> b = ("a".."c").to_a + (0..2).to_a
=> ["a", "b", "c", 0, 1, 2]
How I can do it?
a.flat_map do |string_range|
from, to = string_range.split("..", 2)
(from =~ /^\d+$/ ? (from.to_i..to.to_i) : (from..to)).to_a
end
#=> => ["a", "b", "c", 0, 1, 2]
what about this?
a = ["a..c", "0..2"]
b = a.map { |e| Range.new( *(e).split('..') ).to_a }.flatten
no flat_map used so it works the same on all versions
as #steenslag correctly mentioned, this version does not convert to integers.
here is a version that does:
b = a.map do |e|
Range.new( *(e).split('..').map{ |c| c =~ /\A\d+\Z/ ? c.to_i : c } ).to_a
end.flatten
see it in action here
a = ["a..c", "0..2"]
b = a.flat_map{|str| Range.new(*str.split('..')).to_a} # => ["a", "b", "c", "0", "1", "2"]
p b.map!{|v| Integer(v) rescue v} # => ["a", "b", "c", 0, 1, 2]