How to invert a hash, maintaining duplicate keys - ruby

From an initial hash t:
t = {"1"=>1, "2"=>2, "3"=>2, "6"=>3, "5"=>4, "4"=>1, "8"=>2, "9"=>2, "0"=>1, "7"=>1}
I need to swap the keys and values as follows:
t = {"1"=>1, "2"=>2, "3"=>2, "6"=>3, "5"=>4, "1"=>4, "8"=>2, "9"=>2, "1"=>0, "1"=>7}
While maintaining the structure of the hash (ie, without collapsing duplicate keys).
Then I'll make an array out of this hash.
Is there a way to do this? I tried this:
t.find_all{ |key,value| value == 1 } # pluck all elements with values of 1
#=> [["1", 1], ["4", 1], ["0", 1], ["7", 1]]
But it returns a new array, and the initial hash isn't changed.
The following doesn't work either:
t.invert.find_all{ |key,value| value == 1 }
#=> []

Here's a way to do this:
>> t = {"1" => 1, "2" => 2, "3" => 2, "6" => 3, "5" => 4, "4" => 1, "8" => 2, "9" => 2, "0" => 1, "7" => 1}
Hash#compare_by_identity allows for keys that are duplicates by value but unique by object id:
>> h = Hash.new.compare_by_identity
>> t.each_pair{ |k,v| h[v.to_s] = v.to_i }
The inverse hash of t:
>> h
#=> {"1" => 1, "2" => 2, "2" => 3, "3" => 6, "4" => 5, "1" => 4, "2" => 8, "2" => 9, "1" => 0, "1" => 7}
You can then use find_all to retrieve an array of elements without mutating h:
>> h.find_all{ |k,_| k == "1" }
#=> [["1", 1], ["1", 1], ["1", 1], ["1", 1]]
or keep_if to return the mutated h:
>> h.keep_if{ |k,_| k == "1" }
#=> {"1"=>1, "1"=>1, "1"=>1, "1"=>1}
>> h
#=> {"1"=>1, "1"=>1, "1"=>1, "1"=>1}
Note that this solution assumes you want to maintain the pattern of string keys and integer values in your hash. If you require integer keys, compare_by_identity won't be helpful to you.

Related

Sort hash by key which is a string

Assuming I get back a string:
"27,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,12,17,17,41,17,17,17,17,17,17,17,17,17,17,17,17,17,26,26,26,26,26,26,26,26,26,29,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,40,48,28,28,28,28,28,28,28,28,28,28,28,28,28,28,29,29,29,29,29,29,29,29,29,29,29,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,34,34,34,34,34,34,36,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,40,40,40,40,40,40,40,40,41,41,41,41,41,41,41,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,43,43,43,43,43,43,43,43,43,43,43,43,43,44,44,44,44,48,49,29,41,6,30,11,29,29,36,29,29,36,29,43,1,29,29,29,1,41"
I turn that into an array by calling
str.split(',')
Then turning it into a hash by calling
arr.compact.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }
I would get back a hash that looks like
{"1"=>2, "6"=>1, "39"=>23, "36"=>23, "34"=>39, "32"=>31, "30"=>18, "3"=>8, "2"=>10, "28"=>36, "29"=>21, "26"=>41, "27"=>48, "49"=>1, "44"=>4, "43"=>14, "42"=>34, "48"=>2, "40"=>9, "41"=>10, "11"=>1, "17"=>15, "12"=>1}
However, I'd like to sort that hash by key.
I've tried the solutions listed here.
I believe my problem is related to the fact they keys are strings.
The closest I got was using
Hash[h.sort_by{|k,v| k.to_i}]
Hashes shouldn't be treated as a sorted data structure. They have other advantages and use case as to return their values sequentially. As Mladen Jablanović already pointed out a array of tuples might be the better data structure when you need a sorted key/value pair.
But in current versions of Ruby there actually exists a certain order in which key/value pairs are returned when you call for example each on a hash and that is the order of insertion. Using this behavior you can just build a new hash and insert all key/value pairs into that new hash in the order you want them to be. But keep in mind that the order will break when you add more entries later on.
string = "27,2,2,2,2,2,2,2,2,2,2,3,3,3,3,3,3,3,3,12,17,17,41,17,17,17,17,17,17,17,17,17,17,17,17,17,26,26,26,26,26,26,26,26,26,29,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,26,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,27,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,40,48,28,28,28,28,28,28,28,28,28,28,28,28,28,28,29,29,29,29,29,29,29,29,29,29,29,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,32,34,34,34,34,34,34,36,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,34,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,36,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,39,40,40,40,40,40,40,40,40,41,41,41,41,41,41,41,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,42,43,43,43,43,43,43,43,43,43,43,43,43,43,44,44,44,44,48,49,29,41,6,30,11,29,29,36,29,29,36,29,43,1,29,29,29,1,41"
sorted_number_count_tupels = string.split(',').
group_by(&:itself).
map { |k, v| [k, v.size] }.
sort_by { |(k, v)| k.to_i }
#=> [["1",2],["2",10],["3",8],["6",1],["11",1],["12",1],["17",15],["26",41],["27",48],["28",36],["29",21],["30",18],["32",31],["34",39],["36",23],["39",23],["40",9],["41",10],["42",34],["43",14],["44",4],["48",2],["49",1]]
sorted_number_count_hash = sorted_number_count_tupels.to_h
#=> { "1" => 2, "2" => 10, "3" => 8, "6" => 1, "11" => 1, "12" => 1, "17" => 15, "26" => 41, "27" => 48, "28" => 36, "29" => 21, "30" => 18, "32" => 31, "34" => 39, "36" => 23, "39" => 23, "40" => 9, "41" => 10, "42" => 34, "43" => 14, "44" => 4, "48" => 2, "49" => 1}
Suppose you started with
str = "27,2,2,2,41,26,26,26,48,48,41,6,11,1,41"
and created the following hash
h = str.split(',').inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }
#=> {"27"=>1, "2"=>3, "41"=>3, "26"=>3, "48"=>2, "6"=>1, "11"=>1, "1"=>1}
I removed compact because the array str.split(',') contains only (possibly empty) strings, no nils.
Before continuing, you may want to change this last step to
h = str.split(/\s*,\s*/).each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
#=> {"27"=>1, "2"=>3, "41"=>3, "26"=>3, "48"=>2, "6"=>1, "11"=>1, "1"=>1}
Splitting on the regex allows for the possibility of one or more spaces before or after each comma, and Enumerable#each_with_object avoids the need for that pesky ; h. (Notice the block variables are reversed.)
Then
h.sort_by { |k,_| k.to_i }.to_h
#=> {"1"=>1, "2"=>3, "6"=>1, "11"=>1, "26"=>3, "27"=>1, "41"=>3, "48"=>2}
creates a new hash that contains h's key-value pairs sorted by the integer representations of the keys. See Hash#sort_by.
Notice we've created two hashes. Here's a way to do that by modifying h in place.
h.keys.sort_by(&:to_i).each { |k| h[k] = h.delete(k) }
#=> ["1", "2", "6", "11", "26", "27", "41", "48"] (each always returns the receiver)
h #=> {"1"=>1, "2"=>3, "6"=>1, "11"=>1, "26"=>3, "27"=>1, "41"=>3, "48"=>2}
Lastly, another alternative is to sort str.split(',') before creating the hash.
str.split(',').sort_by(&:to_i).each_with_object(Hash.new(0)) { |e,h| h[e] += 1 }
#=> {"1"=>1, "2"=>3, "6"=>1, "11"=>1, "26"=>3, "27"=>1, "41"=>3, "48"=>2}
Notes
compact
String#split cannot return a nil element. compact won't be useful, here. split might return an empty string, though :
p "1,,2,3".split(',')
# ["1", "", "2", "3"]
p "1,,2,3".split(',').compact
# ["1", "", "2", "3"]
p "1,,2,3".split(',').reject(&:empty?)
# ["1", "2", "3"]
inject
If you have to use two statements inside inject block, each_with_object might be a better idea :
arr.compact.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }
can be rewritten :
arr.compact.each_with_object(Hash.new(0)) { |e, h| h[e] += 1 }
Hash or Array?
If you need to sort results, an Array of pairs might be more suitable than a Hash.
String or Integer?
If you accept to have an integer as key, it might make your code easier to write.
Refactoring
Here's a possibility to rewrite your code :
str.split(',')
.reject(&:empty?)
.map(&:to_i)
.group_by(&:itself)
.map { |k, v| [k, v.size] }
.sort
It outputs :
[[1, 2], [2, 10], [3, 8], [6, 1], [11, 1], [12, 1], [17, 15], [26, 41], [27, 48], [28, 36], [29, 21], [30, 18], [32, 31], [34, 39], [36, 23], [39, 23], [40, 9], [41, 10], [42, 34], [43, 14], [44, 4], [48, 2], [49, 1]]
If you really want a Hash, you can add .to_h :
{1=>2, 2=>10, 3=>8, 6=>1, 11=>1, 12=>1, 17=>15, 26=>41, 27=>48, 28=>36, 29=>21, 30=>18, 32=>31, 34=>39, 36=>23, 39=>23, 40=>9, 41=>10, 42=>34, 43=>14, 44=>4, 48=>2, 49=>1}
You can assign the arr.compact.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h } to a variable and sort it by key:
num = arr.compact.inject(Hash.new(0)) { |h, e| h[e] += 1 ; h }
num.keys.sort
That would sort the hash by key.
A Ruby hash will keep the order of keys added. If the array is small enough to sort I would just change
str.split(',').
to
str.split(',').sort_by(&:to_i)
in order to get the values, and therefore also you hash sorted...

Sort hash by array

I have a hash and an array with same length like the following:
h = {:a => 1, :b => 2, :c => 3, :d => 4}
a = [2, 0, 1, 0]
I want to order the hash in increasing order of the values in the array. So the output would be something like:
h = {:b => 2, :d => 4, :c=> 3, :a => 1}
Ideally I want to introduce some randomness for ties. For the previous example, I want either the previous output or:
h = {:d => 4, :b => 2, :c=> 3, :a => 1}
This is what I tried.
b = a.zip(h).sort.map(&:last)
p Hash[b]
# => {:b=>2, :d=>4, :c=>3, :a=>1}
But I am not sure how to introduce the randomness.
h.to_a.sort_by.each_with_index{|el,i| [a[i], rand]}.to_h
You could modify what you have slightly:
def doit(h,a)
Hash[a.zip(h).sort_by { |e,_| [e,rand] }.map(&:last)]
end
doit(h,a) #=> { b=>2, d=>4, c=>3, a=>1 }
doit(h,a) #=> { d=>4, b=>2, c=>3, a=>1 }
doit(h,a) #=> { b=>2, d=>4, c=>3, a=>1 }
doit(h,a) #=> { b=>2, d=>4, c=>3, a=>1 }

Reverse mapping an array of hash to a hash

I have an Array that contains Hash with only one key and value as anArray.
Eg:
a = [{1 => ["foo", "bar"]}, {2 => ["hello"]}, {3 => ["world", "bar"]}]
Now I want to create a Hash having values of above Hashes as key and their keys as values.
Eg: (desired result)
res = {"foo"=>[1], "bar"=>[1, 3], "hello"=>[2], "world"=>[3]}
I have solved this is following way:
b = Hash.new { |h, k| h[k] = [] }
a.each { |hash|
hash.each { |key, vals|
vals.each { |val|
b[val] << key
}
}
}
b
# => {"foo"=>[1], "bar"=>[1, 3], "hello"=>[2], "world"=>[3]}
It works fine but there should be a better, shorter way to do this than iterating so many times. Please suggest.
One could invert keys and values and then collect elements as needed:
a = [{1 => ["foo", "bar"]}, {2 => ["hello"]}, {3 => ["world", "bar"]}]
a.map(&:invert).inject({}) { |memo,el|
el = el.flatten # hash of size 1 ⇒ array [k ⇒ v]
el.first.each { |k| (memo[k] ||= []) << el.last }
memo
}
#⇒ {
# "bar" => [
# [0] 1,
# [1] 3
# ],
# "foo" => [
# [0] 1
# ],
# "hello" => [
# [0] 2
# ],
# "world" => [
# [0] 3
# ]
# }
Hope it helps.
I'd use group_by, but you need to massage your data:
a.flat_map(&:to_a) # => [[1, ["foo", "bar"]], [2, ["hello"]], [3, ["world", "bar"]]]
.flat_map{|key, values| values.map{|v| [key, v]}} # => [[1, "foo"], [1, "bar"], [2, "hello"], [3, "world"], [3, "bar"]]
.group_by(&:last) # => {"foo"=>[[1, "foo"]], "bar"=>[[1, "bar"], [3, "bar"]], "hello"=>[[2, "hello"]], "world"=>[[3, "world"]]}
.map{|key, values| [key, values.map(&:first)]} # => [["foo", [1]], ["bar", [1, 3]], ["hello", [2]], ["world", [3]]]
.to_h # => {"foo"=>[1], "bar"=>[1, 3], "hello"=>[2], "world"=>[3]}

Ruby printing all elements of a hash but last

Say I have:
hash = {"a" => 1, "b" => 2, "c" => 3}
keys = hash.keys
In order to print all the keys you would do:
keys.each {|x| puts x}
My question is, how do you print all the keys BUT the last one?
I'd do as below using Enumerable#take :
hash = {"a" => 1, "b" => 2, "c" => 3}
hash.take(hash.size-1).each do |k,_|
p k
end
# >> "a"
# >> "b"
Or, as below :
hash = {"a" => 1, "b" => 2, "c" => 3}
hash.keys.take(hash.size-1) # => ["a", "b"]
puts hash.keys.take(hash.size-1)
# >> a
# >> b
update (As asked by OP - Alright now how do I print just the last element explicitly?)
hash = {"a" => 1, "b" => 2, "c" => 3}
hash.keys.last # => "c"
Hash#keys will give you all the keys of that hash as an array. So, now you can call Array#last on that array to get the last element.
hash = {"a" => 1, "b" => 2, "c" => 3}
keys = hash.keys
keys[0..-2]
#=> ["a", "b"]
[0..-2] would index the array from the first to the one before the last
keys[0..-2].each { |x| puts x }
the last element in an array can be retrieved by calling last method on the array
keys.last
#=> "c"

To find the integer (Fixnum) values in ruby array

I have an array [1, 2, "3", "4", "1a", "abc", "a"] with
pure integers (1, 2),
string formatted integers ("1", "2"),
strings ("a", "b"), and
mixed string numbers ("1a", "2s").
From this, I need to pick up only the integers (including string formatted) 1, 2, "3", "4".
First I tried with to_i:
arr = [1, 2, "3", "4", "1a", "abc", "a"]
arr.map {|x| x.to_i}
# => [1, 2, 3, 4, 1, 0, 0]
but this one converts "1a" to 1, which I don't expect.
Then I tried Integer(item):
arr.map {|x| Integer(x) } # and it turned out to be
# => ArgumentError: invalid value for Integer(): "1a"
Now I am out of straight conversion options here. Finally, I decided to do this way, which converts the value to_i and to_s. So "1" == "1".to_i.to_s is an integer, but not "1a" == "1a".to_i.to_s and "a" == "a".to_i.to_s
arr = arr.map do |x|
if (x == x.to_i.to_s)
x.to_i
else
x
end
end
and
ids, names= arr.partition { |item| item.kind_of? Fixnum }
Now I got the arrays of integers and strings. Is there a simple way to do this?
Similar solution as provided by #maerics, but a bit slimmer:
arr.map {|x| Integer(x) rescue nil }.compact
class Array
def to_i
self.map {|x| begin; Integer(x); rescue; nil; end}.compact
end
end
arr = [1, 2, "3", "4", "1a", "abc", "a"]
arr.to_i # => [1, 2, 3, 4]
something like this:
a = [1,2,"3","4","1a","abc","a"]
irb(main):005:0> a.find_all { |e| e.to_s =~ /^\d+$/ }.map(&:to_i)
=> [1, 2, 3, 4]
Hey, thanks awakening my ruby. Here is my go at this problem:
arr=[1,2,"3","4","1a","abc","a"]
arr.map {|i| i.to_s}.select {|s| s =~ /^[0-9]+$/}.map {|i| i.to_i}
//=> [1, 2, 3, 4]
I noticed most of the answer so far changes the value of "3" and "4" to actual integers.
>> array=[1, 2, "3", "4", "1a", "abc", "a", "a13344a" , 10001, 3321]
=> [1, 2, "3", "4", "1a", "abc", "a", "a13344a", 10001, 3321]
>> array.reject{|x| x.to_s[/[^0-9]/] }
=> [1, 2, "3", "4", 10001, 3321]
#OP, I have not tested my solution exhaustively, but so far it seems to work (of course its done according to provided sample ), so please test thoroughly yourself.
How about this?
[1,2,"3","4","1a","abc","a"].select{|x| x.to_i.to_s == x.to_s}
# => [1, 2, "3", "4"]
Looks pretty simple
arr.select{ |b| b.to_s =~ /\d+$/ }
# or
arr.select{ |b| b.to_s[/\d+$/] }
#=> [1, 2, "3", "4"]

Resources