I have an array of mixed elements, e.g. integers and strings:
ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]
and I want to apply a sort but only to specific elements. In the above example, the elements to-be-sorted are the integers (but it could be anything). However, even though they should be sorted, they have to stay in their "integer spots". And the strings have to remain in their exact positions.
The sort should work like this:
[3, "foo", 2, 5, "bar", 1, "baz", 4] # before
[1, "foo", 2, 3, "bar", 4, "baz", 5] # after
"foo" is still at index 1, "bar" is at index 4 and "baz" is at index 6.
I could partition the array into integers and non-integers along with their positions:
a, b = ary.each_with_index.partition { |e, i| e.is_a?(Integer) }
a #=> [[3, 0], [2, 2], [5, 3], [1, 5], [4, 7]]
b #=> [["foo", 1], ["bar", 4], ["baz", 6]]
sort the integers:
result = a.map(&:first).sort
#=>[1, 2, 3, 4, 5]
And re-insert the non-integers at their original positions:
b.each { |e, i| result.insert(i, e) }
result
#=> [1, "foo", 2, 3, "bar", 4, "baz", 5]
But this seems rather clumsy. I particular dislike having to deconstruct and rebuild the array one string at a time. Is there a more elegant or more direct approach?
Possible solution
ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]
integers = ary.select(&->(el) { el.is_a?(Integer) }).sort
ary.map { |n| n.is_a?(Integer) ? integers.shift : n }
# => [1, "foo", 2, 3, "bar", 4, "baz", 5]
I am not proficient with Ruby. Though I'd like to take a shot at what I could come up with.
The idea is as evoked in my comment.
get the indices of the integers
sort the values of the indices
insert the sorted values back into array
ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]
=> [3, "foo", 2, 5, "bar", 1, "baz", 4]
indices = ary.map.with_index { |item,idx| idx if item.is_a?(Integer) }.compact
=> [0, 2, 3, 5, 7]
values = ary.values_at(*indices).sort
=> [1, 2, 3, 4, 5]
indices.zip(values).each { |idx, val| ary[idx]=val}
=> [[0, 1], [2, 2], [3, 3], [5, 4], [7, 5]]
ary
=> [1, "foo", 2, 3, "bar", 4, "baz", 5]
I have assumed that, as in the example, if arr = ary.dup and arr is modified ary is not mutated. If that is not the case one must work with a deep copy of ary.
A helper:
def sort_object?(e)
e.class == Integer
end
sort_object?(3)
#=> true
sort_object?(-3e2)
#=> true
sort_object?("foo")
#=> false
ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]
obj_to_idx = ary.zip((0..ary.size-1).to_a).to_h
#=> {3=>0, "foo"=>1, 2=>2, 5=>3, "bar"=>4, 1=>5, "baz"=>6, 4=>7}
to_sort = ary.select { |k| sort_object?(k) }
#=> [3, 2, 5, 1, 4]
sorted = to_sort.sort
#=> [1, 2, 3, 4, 5]
new_idx_to_orig_idx = to_sort.map { |n| obj_to_idx[n] }
.zip(sorted.map { |n| obj_to_idx[n] })
.to_h
#=> {0=>5, 2=>2, 3=>0, 5=>7, 7=>3}
new_idx_to_orig_idx.each_with_object(ary.dup) do |(new_idx,orig_idx),a|
a[new_idx] = ary[orig_idx]
end
#=> [1, "foo", 2, 3, "bar", 4, "baz", 5]
Some of these statements may of course be chained if desired.
In-Place Re-Assignment at Designated Array Indices
You could certainly make this shorter, and perhaps even skip converting things to and from Hash objects, but the intermediate steps there are to show my thought process and make the intent more explicit and debugging a bit easier. Consider the following:
ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]
ints = ary.each_with_index.select { |elem, idx| [elem, idx] if elem.kind_of? Integer }.to_h
ordered_ints = ints.keys.sort.zip(ints.values).to_h
ints.keys.sort.zip(ints.values).each { |elem, idx| ary[idx] = elem }
ary
#=> [1, "foo", 2, 3, "bar", 4, "baz", 5]
The idea here is that we:
Select just the items of the type we want to sort, along with their index within the current Array using #each_with_index. If you don't like #kind_of? you could replace it with #respond_to? or any other selection criteria that makes sense to you.
Sort Array values selected, and then #zip them up along with the index locations we're going to modify.
Re-assign the sorted elements for each index that needs to be replaced.
With this approach, all unselected elements remain in their original index locations within the ary Array. We're only modifying the values of specific Array indices with the re-ordered items.
I will throw my hat in this ring as well.
It appears the other answers rely on the assumption of uniqueness so I took a different route by building a positional transliteration Hash
(Update: took it a little further than necessary)
def sort_only(ary, sort_klass: Integer)
raise ArgumentError unless sort_klass.is_a?(Class) && sort_klass.respond_to?(:<=>)
# Construct a Hash of [Object,Position] => Object
translator = ary
.each_with_index
.with_object({}) {|a,obj| obj[a] = a.first}
.tap do |t|
# select the keys where the value (original element) is a sort_klass
t.filter_map {|k,v| k if v.is_a?(sort_klass)}
.then do |h|
# sort them and then remap these keys to point at the sorted value
h.sort.each_with_index do |(k,_),idx|
t[h[idx]] = k
end
end
end
# loop through the original array with its position index and use
# the transliteration Hash to place them in the correct order
ary.map.with_index {|a,i| translator[[a,i]]}
end
Example: (Working Example)
sort_only([3, "foo", 2, 5, "bar", 1, "baz", 4])
#=> [1, "foo", 2, 3, "bar", 4, "baz", 5]
sort_only([3, 3, "foo", 2, 5, "bar", 1, "baz",12.0,5,Object, 4,"foo", 7, -1, "qux"])
#=> [-1, 1, "foo", 2, 3, "bar", 3, "baz", 12.0, 4, Object, 5, "foo", 5, 7, "qux"]
sort_only([3, "foo", 2, 5,"qux", "bar", 1, "baz", 4], sort_klass: String)
#=> [3, "bar", 2, 5, "baz", "foo", 1, "qux", 4]
For Reference the Second Example produces the following transliteration:
{[3, 0]=>-1, [3, 1]=>1, ["foo", 2]=>"foo", [2, 3]=>2, [5, 4]=>3,
["bar", 5]=>"bar", [1, 6]=>3, ["baz", 7]=>"baz", [12.0, 8]=>12.0,
[5, 9]=>4, [Object, 10]=>Object, [4, 11]=>5, ["foo", 12]=>"foo",
[7, 13]=>5, [-1, 14]=>7, ["qux", 15]=>"qux"}
Below code simply creates two new arrays: numbers and others. numbers are Integer instances from original array, later sorted in descending order. others looks like original array but number spots replaced with nil. at the end push that sorted numbers to nil spots
arr.inject([[], []]) do |acc, el|
el.is_a?(Integer) ? (acc[0]<<el; acc[1]<<nil) : acc[1]<<el; acc
end.tap do |titself|
numbers = titself[0].sort_by {|e| -e }
titself[1].each_with_index do |u, i|
titself[1][i] = numbers.pop unless u
end
end.last
I'm going to answer my own question here with yet another approach to the problem. This solution delegates most work to Ruby's built-in methods and avoids explicit blocks / loops as far as possible.
Starting with the input array:
ary = [3, "foo", 2, 5, "bar", 1, "baz", 4]
You could build a hash of position => element pairs:
hash = ary.each_index.zip(ary).to_h
#=> {0=>3, 1=>"foo", 2=>2, 3=>5, 4=>"bar", 5=>1, 6=>"baz", 7=>4}
extract the pairs having integer value: (or whatever you want to sort)
ints_hash = hash.select { |k, v| v.is_a?(Integer) }
#=> {0=>3, 2=>2, 3=>5, 5=>1, 7=>4}
sort their values: (any way you want)
sorted_ints = ints_hash.values.sort
#=> [1, 2, 3, 4, 5]
build a new mapping for the sorted values:
sorted_ints_hash = ints_hash.keys.zip(sorted_ints).to_h
#=> {0=>1, 2=>2, 3=>3, 5=>4, 7=>5}
update the position hash:
hash.merge!(sorted_ints_hash)
#=> {0=>1, 1=>"foo", 2=>2, 3=>3, 4=>"bar", 5=>4, 6=>"baz", 7=>5}
And voilĂ :
hash.values
#=> [1, "foo", 2, 3, "bar", 4, "baz", 5]
Let say I have this kind of array
a = [
{key: "cat", value: 1},
{key: "dog", value: 2},
{key: "mouse", value: 5},
{key: "rat", value: 3},
{key: "cat", value: 5},
{key: "rat", value: 2},
{key: "cat", value: 1},
{key: "cat", value: 1}
]
Let say I have this array, and want to get only the latest value found for "cat".
I know how to select all of them
like
a.select do |e|
e[:key] == "cat"
end
But I'm looking for a way to just get a selection of the last 3
desired result would be
[
{key: "cat", value: 5},
{key: "cat", value: 1},
{key: "cat", value: 1}
]
thanks!
In a comment on the question #Stefan suggested:
a.select { |e| e[:key] == "cat" }.last(3)
Provided a is not too large that is likely what you should use. However, if a is large, and especially if it contains many elements (hashes) h for which h[:key] #=> "cat", it likely would be more efficient to iterate backwards from the end of the array and terminate ("short-circuit") as soon as three elements h have been found for which h[:key] #=> "cat". This also avoids the construction of a potentially-large temporary array (a.select { |e| e[:key] == "cat" }).
One way to do that is as follows.
a.reverse_each.with_object([]) do |h,arr|
arr.insert(0,h) if h[:key] == "cat"
break arr if arr.size == 3
end
#=> [{:key=>"cat", :value=>5},
# {:key=>"cat", :value=>1},
# {:key=>"cat", :value=>1}]
See Array#reverse_each, Enumerator#with_object and Array#insert. Note that because reverse_each and with_object both return enumerators, chaining them produces an enumerator as well:
a.reverse_each.with_object([])
#=> #<Enumerator: #<Enumerator: [{:key=>"cat", :value=>1},
# ...
# {:key=>"cat", :value=>1}]:reverse_each>:with_object([])>
It might be ever-so-slightly faster to replace the block calculation with
arr << h if h[:key] == "cat"
break arr.reverse if arr.size == 3
If a contains fewer elements h for which h[:key] #=> "cat" an array arr will be returned for which arr.size < 3. It therefore is necessary to confirm that the array returned contains three elements.
This check must also be performed when #Stefan's suggested code is used, as (for example)
a.select { |e| e[:key] == "cat" }.last(99)
#=> [{:key=>"cat", :value=>1},
# {:key=>"cat", :value=>5},
# {:key=>"cat", :value=>1},
# {:key=>"cat", :value=>1}]
So we have and array of hashes
array = [
{id: 1, parent_id: 0},
{id: 2, parent_id: 1},
{id: 3, parent_id: 0},
{id: 4, parent_id: 2}
]
target_array = []
What is the most efficient and ruby way to map/sort that array to the following result:
target_array = [
{id:1,children:
[{id: 2, children: [
{id:4, children:[]}]}]},
{id: 3, children:[]}
]
p.s.The most I am capable of is iterating whole thing for each item and excluding from array hash that is already mapped to target_array.
You can solve this with recursion :
#array = [
{id: 1, parent_id: 0},
{id: 2, parent_id: 1},
{id: 3, parent_id: 0},
{id: 4, parent_id: 2}
]
def build_hierarchy target_array, n
#array.select { |h| h[:parent_id] == n }.each do |h|
target_array << {id: h[:id], children: build_hierarchy([], h[:id])}
end
target_array
end
build_hierarchy [], 0
Output :
=> [{"id"=>1, "children"=>[{"id"=>2, "children"=>[{"id"=>4, "children"=>[]}]}]}, {"id"=>3, "children"=>[]}]
Live example in this ruby fiddle http://rubyfiddle.com/riddles/9b643
I would use recursion, but the following could easily be converted to a non-recursive method.
First construct a hash linking parents to their children (p2c). For this, use the form of Hash#update (aka merge!) that uses a block to determine the values of keys that are present in both hashes being merged:
#p2c = array.each_with_object({}) { |g,h|
h.update(g[:parent_id]=>[g[:id]]) { |_,ov,nv| ov+nv } }
#=> {0=>[1, 3], 1=>[2], 2=>[4]}
There are many other ways to construct this hash. Here's another:
#p2c = Hash[array.group_by { |h| h[:parent_id] }
.map { |k,v| [k, v.map { |g| g[:id] }] }]
Now construct a recursive method whose lone argument is a parent:
def family_tree(p=0)
return [{ id: p, children: [] }] unless #p2c.key?(p)
#p2c[p].each_with_object([]) { |c,a|
a << { id:c, children: family_tree(c) } }
end
We obtain:
family_tree
#=> [ { :id=>1, :children=>
# [
# { :id=>2, :children=>
# [
# { :id=>4, :children=>[] }
# ]
# }
# ]
# },
# { :id=>3, :children=>[] }
# ]
Constructing the hash #p2c initially should make it quite efficient.
This is what I tried my way using Hash
array = [
{id: 1, parent_id: 0},
{id: 2, parent_id: 1},
{id: 3, parent_id: 0},
{id: 4, parent_id: 2}
]
target_hash = Hash.new { |h,k| h[k] = { id: nil, children: [ ] } }
array.each do |n|
id, parent_id = n.values_at(:id, :parent_id)
target_hash[id][:id] = n[:id]
target_hash[parent_id][:children].push(target_hash[id])
end
puts target_hash[0]
Output:
{:id=>nil, :children=>[{:id=>1, :children=>[{:id=>2, :children=>[{:id=>4, :children=>[]}]}]}, {:id=>3, :children=>[]}]}
I think the best one will have O(nlog(n)) time complexity at most. I'm giving my non-hash one :
array = [
{id: 1, parent_id: 0},
{id: 2, parent_id: 1},
{id: 3, parent_id: 0},
{id: 4, parent_id: 2}
]
# This takes O(nlog(n)).
array.sort! do |a, b|
k = (b[:parent_id] <=> b[:parent_id])
k == 0 ? b[:id] <=> a[:id] : k
end
# This takes O(n)
target_array = array.map do |node|
{ id: node[:id], children: [] }
end
# This takes O(nlog(n))
target_array.each_with_index do |node, index|
parent = target_array[index + 1...target_array.size].bsearch do |target_node|
target_node[:id] == array[index][:parent_id]
end
if parent
parent[:children] << node
target_array[index] = nil
end
end
# O(n)
target_array.reverse.compact
# =>
# [{:id => 1, :children =>[{:id=>2,:children=> [ {:id=>4,
# :children=>[]}]}]},
# {:id=>3, :children=>[]} ]
So mine uses O(nlog(n)) in general.
By the way, when I simply tested out the existing solutions I found Gagan Gami's to be most efficient (slightly ahead of mine), I believe it's O(nlog(n)) too, though not obvious. But the currently accepted solution takes O(n^2) time.