2

I'm new to Ruby and looking to sort only certain items in my collection. For example if I have the following array. I only want to sort the objects that contains the property type: 'sort'

   object = [{
            type: 'sort',
            id: 3
        }, {
            type: 'notsort',
            id: 4
        }, {
            type: 'sort',
            id: 1
        }, {
            type: 'sort',
            id: 0
        }
    ]

I need the order to map directly to the id map below.

sortIdOrder = [0, 1, 3]

The end result should look like:

object = [{
    type: 'notsort',
    id: 4
}, {
    type: 'sort',
    id: 0
},{
    type: 'sort',
    id: 1
}, {
    type: 'sort',
    id: 3
}]

As you can see the array is sorted by id based on the sortIdOrder . The notsort type can either be at the end or start.

1
  • As a note, convention holds that Ruby methods and variables should be of the form sort_id_order. Commented Sep 20, 2016 at 4:05

5 Answers 5

3

Sorting can be expensive, so one should not sort when the desired order is known, as it is here.

I've assumed that the values :id are unique, as the question would not make sense if they were not.

First partition the hashes into those to be sorted and the rest.

sortees, nonsortees = object.partition { |h| h[:type] == 'sort' }
  #=> [[{:type=>"sort", :id=>3}, {:type=>"sort", :id=>1}, {:type=>"sort", :id=>0}],
  #    [{:type=>"notsort", :id=>4}]] 

so

sortees
  #=> [{:type=>"sort", :id=>3}, {:type=>"sort", :id=>1}, {:type=>"sort", :id=>0}]
nonsortees
  #=> [{:type=>"notsort", :id=>4}] 

I'll put the elements of sortees in the desired order then concatenate that array with nonsortees, putting the hashes that are not to be sorted at the end.

I order the elements of sortees by creating a hash with one key-value pair g[:id]=>g for each element g (a hash) of sortees. That allows me to use Hash#values_at to pull out the desired hashes in the specified order.

sortees.each_with_object({}) { |g,h| h[g[:id]] = g }.
        values_at(*sortIdOrder).
        concat(nonsortees)
  #=> [{:type=>"sort", :id=>0}, {:type=>"sort", :id=>1}, {:type=>"sort", :id=>3},
  #    {:type=>"notsort", :id=>4}]

Note that

sortees.each_with_object({}) { |g,h| h[g[:id]] = g }
  #=> {3=>{:type=>"sort", :id=>3}, 1=>{:type=>"sort", :id=>1},
  #    0=>{:type=>"sort", :id=>0}} 
Sign up to request clarification or add additional context in comments.

2 Comments

You know, sort_by with an index_of call might be the way to go here. For larger lists you'd certainly want to convert the ID order array to a hash: Hash[array.each_with_index.to_a].invert could do it easily.
@tadman, that's a nice use of each_with_index and invert. I must tuck that away.
0

A not very performant one-liner:

object.sort_by{|o| sortIdOrder.index(o[:id]) || -1}

This makes the notsort objects appear at the head of sorted array. It's an O(m * nlog(n)) algorithm, where n is the size of object and m is the size of sortIdOrder. This is faster when your object and sortIdOrder are small.

A more performant one for large arrays is

order = sortIdOrder.each.with_index.with_object(Hash.new(-1)) {|(id, index), h| h[id] = index}

object.sort_by{|o| order[o[:id]]}

This is an O(m + nlog(n)) algorithm but requires more memory.

Comments

0

you can use sort with a block that sorts by :type, then :id.

object.sort {|a, b| [a[:type], a[:id]] <=> [b[:type], b[:id]] }

[{:type=>"notsort", :id=>4}, 
 {:type=>"sort", :id=>0}, 
 {:type=>"sort", :id=>1}, 
 {:type=>"sort", :id=>3}]

2 Comments

Thanks but I would like the order to be determined by the sortIdOrder. Would I just change the solution to: object.sort {|a, b| [a[:type], sortIdOrder.index(a[:id])] <=> [b[:type], sortIdOrder.index(b[:id])] } ?
Cool. Some cases type can be nil though. Would this not work in that case?
0

I'd go with something like this:

object.sort_by do |o|
  [
    (o[:type] == :sort) ? 0 : 1,
    sortIdOrder.index(o[:id])
  ]
end

When sorting by an array, you're essentially sorting by the first element, except when they're the same, in which case you sort by the second element, etc. In code above, (o[:type] == :sort) ? 0 : 1 ensures that everything with a type of :sort comes first, and everything else after, even if the type is nil, or 5 or whatever you like. The sortIdOrder.index(o[:id]) term ensures things are sorted as you like (though items with no :id, or whose :id is not found in sortIdOrder will be ordered arbitrarily. If your data set is very large, you may want to tweak this further so that the sortIdOrder array is not performed for non-sort items.

Enumerable#sort_by only has to call the block once for each element, and then perform fast comparisons on the results; Enumerable#sort has to call the block on pairs of elements, which means it's called more often:

irb(main):015:0> ary = %w{9 8 7 6 5 4 3 2 1}
=> ["9", "8", "7", "6", "5", "4", "3", "2", "1"]
irb(main):016:0> a = 0; ary.sort_by {|x| puts x; a+=1; x.to_i }; puts "Total: #{a}"
9
8
7
6
5
4
3
2
1
Total: 9
=> nil
irb(main):017:0> a = 0; ary.sort {|x,y| puts "#{x},#{y}"; a+=1; x.to_i <=> y.to_i }; puts "Total: #{a}"
9,5
5,1
8,5
2,5
7,5
3,5
6,5
4,5
6,8
8,9
7,8
6,7
1,3
3,4
2,3
1,2
Total: 16
=> nil

In these cases, it's really not very important, because hash access is fast anyway (though sort_by is still more legible), but in cases where calculating the attribute you want to sort by is even moderately expensive, sort_by can be quite a bit faster. The block form of sort is mostly useful if the comparison logic itself is complicated.

2 Comments

Thanks for answer. What if :type is nil ?
If :type might be nil, or if you have many different types besides :sort and :nosort and you want to be sure that everything but sort ends up grouped together, you'd be better off using [(o[:type] == :sort ? 0 : 1), sortIdOrder.index(o[:id]) as your sort criterion. That'l put all of the :sorts at the front of the list (in the desired sort order) and everything else afterwards (in potentially arbitrary order, if their ids are missing from the sortIdOrder array. I'll update the answer to reflect that.
0

Maybe I'm late, but my solution is:

Rails Solution:

object.partition { |hash| hash[:id].in?(sortIdOrder) }.flatten.reverse

Ruby Solution:

object.partition { |hash| sortIdOrder.include? hash[:id] }.flatten.reverse

both of it in result give this:

=> [{:type=>"notsort", :id=>4},
     {:type=>"sort", :id=>0},
     {:type=>"sort", :id=>1},
     {:type=>"sort", :id=>3}]

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.