Separate list items by their class or kind_of in ruby - ruby

I have two lists:
a = [1,2,3]
b = ["a","b","c"]
my list l is:
l = [a,b].flatten
so l = [1,2,3,"a","b","c"]
I'm looking for an elegant way of splitting the list by the type of the items in it, in order to have a and b restored as they were.
I could go with each item in the list and test, but that doesn't seem efficient runtime-wise nor code-wise.

You could use group_by and then pull your arrays out of the resulting Hash:
>> by_class = l.group_by(&:class)
=> {Integer=>[1, 2, 3], String=>["a", "b", "c"]}
>> a = by_class[Fixnum]
=> [1, 2, 3]
>> b = by_class[String]
=> ["a", "b", "c"]
If you know that you only have Fixnums and Strings then you could use partition:
>> a, b = *l.partition { |o| o.is_a? Fixnum }
=> [[1, 2, 3], ["a", "b", "c"]]
>> a
=> [1, 2, 3]
>> b
=> ["a", "b", "c"]

Related

Convert array into a hash

I try to learn map and group_by but it's difficult...
My array of arrays :
a = [ [1, 0, "a", "b"], [1, 1, "c", "d"], [2, 0, "e", "f"], [3, 1, "g", "h"] ]
Expected result :
b= {
1=> {0=>["a", "b"], 1=>["c", "d"]} ,
2=> {0=>["e", "f"]} ,
3=> {1=>["g", "h"]}
}
Group by the first value, the second value can just be 0 or 1.
A starting :
a.group_by{ |e| e.shift}.map { |k, v| {k=>v.group_by{ |e| e.shift}} }
=> [{1=>{0=>[["a", "b"]], 1=>[["c", "d"]]}},
{2=>{0=>[["e", "f"]]}}, {3=>{1=>[["g", "h"]]}}]
I want to get "a" and "b" with the 2 first values, it's the only solution that I've found... (using a hash of hash)
Not sure if group_by is the simplest solution here:
a = [ [1, 0, "a", "b"], [1, 1, "c", "d"], [2, 0, "e", "f"], [3, 1, "g", "h"] ]
result = a.inject({}) do |acc,(a,b,c,d)|
acc[a] ||= {}
acc[a][b] = [c,d]
acc
end
puts result.inspect
Will print:
{1=>{0=>["a", "b"], 1=>["c", "d"]}, 2=>{0=>["e", "f"]}, 3=>{1=>["g", "h"]}}
Also, avoid changing the items you're operating on directly (the shift calls), the collections you could be receiving in your code might not be yours to change.
If you want a somewhat custom group_by I tend do just do it manually. group_by creates an Array of grouped values, so it creates [["a", "b"]] instead of ["a", "b"]. In addition your code is destructive, i.e. it manipulates the value of a. That is only a bad thing if you plan on re using a later on in its original form, but important to note.
As I mentioned though, you might as well just loop through a once and build the desired structure instead of doing multiple group_bys.
b = {}
a.each do |aa|
(b[aa[0]] ||= {})[aa[1]] = aa[2..3]
end
b # => {1=>{0=>["a", "b"], 1=>["c", "d"]}, 2=>{0=>["e", "f"]}, 3=>{1=>["g", "h"]}}
With (b[aa[0]] ||= {}) we check for the existence of the key aa[0] in the Hash b. If it does not exist, we assign an empty Hash ({}) to that key. Following that, we insert the last two elements of aa (= aa[2..3]) into that Hash, with aa[1] as key.
Note that this does not account for duplicate primary + secondary keys. That is, if you have another entry [1, 1, "x", "y"] it will overwrite the entry of [1, 1, "c", "d"] because they both have keys 1 and 1. You can fix that by storing the values in an Array, but then you might as well just do a double group_by. For example, with destructive behavior on a, handling "duplicates":
# Added [1, 1, "x", "y"], removed some others
a = [ [1, 0, "a", "b"], [1, 1, "c", "d"], [1, 1, "x", "y"] ]
b = Hash[a.group_by(&:shift).map { |k, v| [k, v.group_by(&:shift) ] }]
#=> {1=>{0=>[["a", "b"]], 1=>[["c", "d"], ["x", "y"]]}}
[[1, 0, "a", "b"], [1, 1, "c", "d"], [2, 0, "e", "f"], [3, 1, "g", "h"]].
group_by{ |e| e.shift }.
map{ |k, v| [k, v.inject({}) { |h, v| h[v.shift] = v; h }] }.
to_h
#=> {1=>{0=>["a", "b"], 1=>["c", "d"]}, 2=>{0=>["e", "f"]}, 3=>{1=>["g", "h"]}}
Here's how you can do it (nondestructively) with two Enumerable#group_by's and an Object#tap. The elements of a (arrays) could could vary in size and the size of each could be two or greater.
Code
def convert(arr)
h = arr.group_by(&:first)
h.keys.each { |k| h[k] = h[k].group_by { |a| a[1] }
.tap { |g| g.keys.each { |j|
g[j] = g[j].first[2..-1] } } }
h
end
Example
a = [ [1, 0, "a", "b"], [1, 1, "c", "d"], [2, 0, "e", "f"], [3, 1, "g", "h"] ]
convert(a)
#=> {1=>{0=>["a", "b"], 1=>["c", "d"]}, 2=>{0=>["e", "f"]}, 3=>{1=>["g", "h"]}}
Explanation
h = a.group_by(&:first)
#=> {1=>[[1, 0, "a", "b"], [1, 1, "c", "d"]],
# 2=>[[2, 0, "e", "f"]],
# 3=>[[3, 1, "g", "h"]]}
keys = h.keys
#=> [1, 2, 3]
The first value of keys passed into the block assigns the value 1 to the block variable k. We will set h[1] to a hash f, computed as follows.
f = h[k].group_by { |a| a[1] }
#=> [[1, 0, "a", "b"], [1, 1, "c", "d"]].group_by { |a| a[1] }
#=> {0=>[[1, 0, "a", "b"]], 1=>[[1, 1, "c", "d"]]}
We need to do further processing of this hash, so we capture it with tap and assign it to tap's block variable g (i.e., g will initially equal f above). g will be returned by the block after modification.
We have
g.keys #=> [0, 1]
so 0 is the first value passed into each's block and assigned to the block variable j. We then compute:
g[j] = g[j].first[2..-1]
#=> g[0] = [[1, 0, "a", "b"]].first[2..-1]
#=> ["a", "b"]
Similarly, when g's second key (1) is passed into the block,
g[j] = g[j].first[2..-1]
#=> g[1] = [[1, 1, "c", "d"]].first[2..-1]
#=> ["c", "d"]
Ergo,
h[1] = g
#=> {0=>["a", "b"], 1=>["c", "d"]}
h[2] and h[3] are computed similarly, giving us the desired result.

How can I push keys to an unsorted array?

I'm trying to create an array of the keys of an ordered hash. I want them to be listed in the same order in both the array and the hash. I have this hash.
h = { "a" => 3, "b" => 1, "c" = 4, "d" = 2 }
What I want is this array.
arr = ["b", "d", "a", "c"]
I have
h.sort_by { |k, v| v}
h.keys
but that returns the keys in alphabetical order. What can I do to keep them in the order of the sorted hash?
h.sort_by{|k,v| v} will give you [["b", 1], ["d", 2], ["a", 3], ["c", 4]], then use .map to get the key.
h.sort_by{|k,v| v}.map &:first
h = { "a" => 3, "b" => 1, "c" => 4, "d" => 2 }
p h.sort_by(&:last).map(&:first) #=> ["b", "d", "a", "c"]
You may try this also,
h = { "a" => 3, "b" => 1, "c" => 4, "d" => 2 }
Hash[h.sort_by{|k,v| v}].keys
#=> ["b", "d", "a", "c"]
This code
h.sort_by { |k,v| v}
h.keys
doesn't work because the sort_by method doesn't sort the original array, it returns a new sorted array, where each value is a (key, value) pair from the original hash:
[["b", 1], ["d", 2], ["a", 3], ["c", 4]]
If you're using Ruby 2.1.1, you can then just call to_h on the array, which will re-map the key/value pairs back into a hash:
h.sort_by { |k, v| v}.to_h.keys

Reposition an element to the front of an array in Ruby

Even coming from javascript this looks atrocious to me:
irb
>> a = ['a', 'b', 'c']
=> ["a", "b", "c"]
>> a.unshift(a.delete('c'))
=> ["c", "a", "b"]
Is there a more legible way placing an element to the front of an array?
Edit my actual code:
if #admin_users.include?(current_user)
#admin_users.unshift(#admin_users.delete(current_user))
end
Maybe this looks better to you:
a.insert(0, a.delete('c'))
Maybe Array#rotate would work for you:
['a', 'b', 'c'].rotate(-1)
#=> ["c", "a", "b"]
This is a trickier problem than it seems. I defined the following tests:
describe Array do
describe '.promote' do
subject(:array) { [1, 2, 3] }
it { expect(array.promote(2)).to eq [2, 1, 3] }
it { expect(array.promote(3)).to eq [3, 1, 2] }
it { expect(array.promote(4)).to eq [1, 2, 3] }
it { expect((array + array).promote(2)).to eq [2, 1, 3, 1, 2, 3] }
end
end
sort_by proposed by #Duopixel is elegant but produces [3, 2, 1] for the second test.
class Array
def promote(promoted_element)
sort_by { |element| element == promoted_element ? 0 : 1 }
end
end
#tadman uses delete, but this deletes all matching elements, so the output of the fourth test is [2, 1, 3, 1, 3].
class Array
def promote(promoted_element)
if (found = delete(promoted_element))
unshift(found)
end
self
end
end
I tried using:
class Array
def promote(promoted_element)
return self unless (found = delete_at(find_index(promoted_element)))
unshift(found)
end
end
But that failed the third test because delete_at can't handle nil. Finally, I settled on:
class Array
def promote(promoted_element)
return self unless (found_index = find_index(promoted_element))
unshift(delete_at(found_index))
end
end
Who knew a simple idea like promote could be so tricky?
Adding my two cents:
array.select{ |item| <condition> } | array
Pros:
Can move multiple items to front of array
Cons:
This will remove all duplicates unless it's the desired outcome.
Example - Move all odd numbers to the front (and make array unique):
data = [1, 2, 3, 4, 3, 5, 1]
data.select{ |item| item.odd? } | data
# Short version:
data.select(&:odd?) | data
Result:
[1, 3, 5, 2, 4]
Another way:
a = [1, 2, 3, 4]
b = 3
[b] + (a - [b])
=> [3, 1, 2, 4]
If by "elegant" you mean more readable even at the expense of being non-standard, you could always write your own method that enhances Array:
class Array
def promote(value)
if (found = delete(value))
unshift(found)
end
self
end
end
a = %w[ a b c ]
a.promote('c')
# => ["c", "a", "b"]
a.promote('x')
# => ["c", "a", "b"]
Keep in mind this would only reposition a single instance of a value. If there are several in the array, subsequent ones would probably not be moved until the first is removed.
In the end I considered this the most readable alternative to moving an element to the front:
if #admin_users.include?(current_user)
#admin_users.sort_by{|admin| admin == current_user ? 0 : 1}
end
If all the elements in the array are unique you can use array arithmetic:
> a = ['a', 'b', 'c']
=> ["a", "b", "c"]
> a -= "c"
=> ["a", "b"]
> a = ["c"] + a
=> ["c", "a", "b"]
Building on above:
class Array
def promote(*promoted)
self - (tail = self - promoted) + tail
end
end
[1,2,3,4].promote(5)
=> [1, 2, 3, 4]
[1,2,3,4].promote(4)
=> [4, 1, 2, 3]
[1,2,3,4].promote(2,4)
=> [2, 4, 1, 3]

Reordering an array in the same order as another array was reordered

I have two arrays a, b of the same length:
a = [a_1, a_2, ..., a_n]
b = [b_1, b_2, ..., b_n]
When I sort a using sort_by!, the elements of a will be arranged in different order:
a.sort_by!{|a_i| some_condition(a_i)}
How can I reorder b in the same order/rearrangement as the reordering of a? For example, if a after sort_by! is
[a_3, a_6, a_1, ..., a_i_n]
then I want
[b_3, b_6, b_1, ..., b_i_n]
Edit
I need to do it in place (i.e., retain the object_id of a, b). The two answers given so far is useful in that, given the sorted arrays:
a_sorted
b_sorted
I can do
a.replace(a_sorted)
b.replace(b_sorted)
but if possible, I want to do it directly. If not, I will accept one of the answers already given.
One approach would be to zip the two arrays together and sort them at the same time. Something like this, perhaps?
a = [1, 2, 3, 4, 5]
b = %w(a b c d e)
a,b = a.zip(b).sort_by { rand }.transpose
p a #=> [3, 5, 2, 4, 1]
p b #=> ["c", "e", "b", "d", "a"]
How about:
ary_a = [ 3, 1, 2] # => [3, 1, 2]
ary_b = [ 'a', 'b', 'c'] # => ["a", "b", "c"]
ary_a.zip(ary_b).sort{ |a,b| a.first <=> b.first }.map{ |a,b| b } # => ["b", "c", "a"]
or
ary_a.zip(ary_b).sort_by(&:first).map{ |a,b| b } # => ["b", "c", "a"]
If the entries are unique, the following may work. I haven't tested it. This is partially copied from https://stackoverflow.com/a/4283318/38765
temporary_copy = a.sort_by{|a_i| some_condition(a_i)}
new_indexes = a.map {|a_i| temporary_copy.index(a_i)}
a.each_with_index.sort_by! do |element, i|
new_indexes[i]
end
b.each_with_index.sort_by! do |element, i|
new_indexes[i]
end

How to count duplicates in Ruby Arrays

How do you count duplicates in a ruby array?
For example, if my array had three a's, how could I count that
Another version of a hash with a key for each element in your array and value for the count of each element
a = [ 1, 2, 3, 3, 4, 3]
h = Hash.new(0)
a.each { | v | h.store(v, h[v]+1) }
# h = { 3=>3, 2=>1, 1=>1, 4=>1 }
Given:
arr = [ 1, 2, 3, 2, 4, 5, 3]
My favourite way of counting elements is:
counts = arr.group_by{|i| i}.map{|k,v| [k, v.count] }
# => [[1, 1], [2, 2], [3, 2], [4, 1], [5, 1]]
If you need a hash instead of an array:
Hash[*counts.flatten]
# => {1=>1, 2=>2, 3=>2, 4=>1, 5=>1}
This will yield the duplicate elements as a hash with the number of occurences for each duplicate item. Let the code speak:
#!/usr/bin/env ruby
class Array
# monkey-patched version
def dup_hash
inject(Hash.new(0)) { |h,e| h[e] += 1; h }.select {
|k,v| v > 1 }.inject({}) { |r, e| r[e.first] = e.last; r }
end
end
# unmonkeey'd
def dup_hash(ary)
ary.inject(Hash.new(0)) { |h,e| h[e] += 1; h }.select {
|_k,v| v > 1 }.inject({}) { |r, e| r[e.first] = e.last; r }
end
p dup_hash([1, 2, "a", "a", 4, "a", 2, 1])
# {"a"=>3, 1=>2, 2=>2}
p [1, 2, "Thanks", "You're welcome", "Thanks",
"You're welcome", "Thanks", "You're welcome"].dup_hash
# {"You're welcome"=>3, "Thanks"=>3}
Simple.
arr = [2,3,4,3,2,67,2]
repeats = arr.length - arr.uniq.length
puts repeats
arr = %w( a b c d c b a )
# => ["a", "b", "c", "d", "c", "b", "a"]
arr.count('a')
# => 2
Another way to count array duplicates is:
arr= [2,2,3,3,2,4,2]
arr.group_by{|x| x}.map{|k,v| [k,v.count] }
result is
[[2, 4], [3, 2], [4, 1]]
requires 1.8.7+ for group_by
ary = %w{a b c d a e f g a h i b}
ary.group_by{|elem| elem}.select{|key,val| val.length > 1}.map{|key,val| key}
# => ["a", "b"]
with 1.9+ this can be slightly simplified because Hash#select will return a hash.
ary.group_by{|elem| elem}.select{|key,val| val.length > 1}.keys
# => ["a", "b"]
To count instances of a single element use inject
array.inject(0){|count,elem| elem == value ? count+1 : count}
arr = [1, 2, "a", "a", 4, "a", 2, 1]
arr.group_by(&:itself).transform_values(&:size)
#=> {1=>2, 2=>2, "a"=>3, 4=>1}
Ruby >= 2.7 solution here:
A new method .tally has been added.
Tallies the collection, i.e., counts the occurrences of each element. Returns a hash with the elements of the collection as keys and the corresponding counts as values.
So now, you will be able to do:
["a", "b", "c", "b"].tally #=> {"a"=>1, "b"=>2, "c"=>1}
What about a grep?
arr = [1, 2, "Thanks", "You're welcome", "Thanks", "You're welcome", "Thanks", "You're welcome"]
arr.grep('Thanks').size # => 3
Its Easy:
words = ["aa","bb","cc","bb","bb","cc"]
One line simple solution is:
words.each_with_object(Hash.new(0)) { |word,counts| counts[word] += 1 }
It works for me.
Thanks!!
I don't think there's a built-in method. If all you need is the total count of duplicates, you could take a.length - a.uniq.length. If you're looking for the count of a single particular element, try
a.select {|e| e == my_element}.length.
Improving #Kim's answer:
arr = [1, 2, "a", "a", 4, "a", 2, 1]
Hash.new(0).tap { |h| arr.each { |v| h[v] += 1 } }
# => {1=>2, 2=>2, "a"=>3, 4=>1}
Ruby code to get the repeated elements in the array:
numbers = [1,2,3,1,2,0,8,9,0,1,2,3]
similar = numbers.each_with_object([]) do |n, dups|
dups << n if seen.include?(n)
seen << n
end
print "similar --> ", similar
Another way to do it is to use each_with_object:
a = [ 1, 2, 3, 3, 4, 3]
hash = a.each_with_object({}) {|v, h|
h[v] ||= 0
h[v] += 1
}
# hash = { 3=>3, 2=>1, 1=>1, 4=>1 }
This way, calling a non-existing key such as hash[5] will return nil instead of 0 with Kim's solution.
I've used reduce/inject for this in the past, like the following
array = [1,5,4,3,1,5,6,8,8,8,9]
array.reduce (Hash.new(0)) {|counts, el| counts[el]+=1; counts}
produces
=> {1=>2, 5=>2, 4=>1, 3=>1, 6=>1, 8=>3, 9=>1}

Resources