Jesper Rasmussen
Jesper Rasmussen

Reputation: 258

Sorting an array by entry properties, with optional grouping

I have an array that looks like this:

[
    {"timestamp" => 1347119549, "category" => nil},
    {"timestamp" => 1347119547, "category" => "Monkeys"},
    {"timestamp" => 1347119543, "category" => nil},
    {"timestamp" => 1347119542, "category" => "Monkeys"}
]

I want to sort it by timestamp (descending), UNLESS it has a category not being nil, in which case it should appear with it's "siblings", even though it is "older" than an uncategorized entry. I need to sort this array, so it appears like this:

[
    {"timestamp" => 1347119549, "category" => nil},
    {"timestamp" => 1347119547, "category" => "Monkeys"},
    {"timestamp" => 1347119542, "category" => "Monkeys"},
    {"timestamp" => 1347119543, "category" => nil}
]

I am trying to figure out how to get the correct result by using group_by and sort, but have had no success.

Upvotes: 2

Views: 121

Answers (4)

Borodin
Borodin

Reputation: 126742

This is simply done using the tools you've tried.

First sort the entire array by tiemstamp, and then allocate them groups by category using group_by:

arr = [
    {'timestamp' => 1347119549, 'category' => nil},
    {'timestamp' => 1347119547, 'category' => 'Monkeys'},
    {'timestamp' => 1347119543, 'category' => nil},
    {'timestamp' => 1347119542, 'category' => 'Monkeys'},
    {'timestamp' => 1347119541, 'category' => nil},
    {'timestamp' => 1347119548, 'category' => nil},
    {'timestamp' => 1347119545, 'category' => nil},
]

sorted = arr.sort_by { |elem| 0 - elem['timestamp'] }
groups = sorted.group_by { |elem| elem['category'] or Object.new }
sorted = groups.values.flatten

puts sorted

output

{"timestamp"=>1347119549, "category"=>nil}
{"timestamp"=>1347119548, "category"=>nil}
{"timestamp"=>1347119547, "category"=>"Monkeys"}
{"timestamp"=>1347119542, "category"=>"Monkeys"}
{"timestamp"=>1347119545, "category"=>nil}
{"timestamp"=>1347119543, "category"=>nil}
{"timestamp"=>1347119541, "category"=>nil}

You could, of course, pipeline the whole thing, at the cost of readability.

sorted = arr.sort_by { |elem| 0 - elem['timestamp'] }.group_by { |elem| elem['category'] or Object.new }.values.flatten

Upvotes: 1

tessi
tessi

Reputation: 13574

It looks a little ugly, but it works:

a = [
  {"timestamp"=>1347119549, "category"=>nil},
  {"timestamp"=>1347119547, "category"=>"Monkeys"},
  {"timestamp"=>1347119543, "category"=>nil},
  {"timestamp"=>1347119542, "category"=>"Monkeys"},
  {"timestamp"=>1347119548, "category"=>"Dog"},
  {"timestamp"=>1347119544, "category"=>"Dog"}
]
groups = a.sort_by {|h| -h['timestamp']}.group_by {|h| h['category']}
sorted = (groups.delete(nil) || []) + groups.values
sorted = sorted.sort_by{|i| i.is_a?(Hash) ? -i['timestamp'] : -i.first['timestamp']}.flatten

This gives you the following in sorted:

[
  {"timestamp"=>1347119549, "category"=>nil},
  {"timestamp"=>1347119548, "category"=>"Dog"},
  {"timestamp"=>1347119544, "category"=>"Dog"},
  {"timestamp"=>1347119547, "category"=>"Monkeys"},
  {"timestamp"=>1347119542, "category"=>"Monkeys"},
  {"timestamp"=>1347119543, "category"=>nil}
]

I sort first by 'timestamp', so that the groups are sorted later.

After grouping by 'category', I move the values of the nil category in an array. Here, I use (groups.delete(nil) || []) in case the nil group is empty.

Now it can be sorted by 'timestamp' again, with the timestamp of an array being the timestamp of its first hash.

Finally flatten gives us the desired array.

Upvotes: 1

Neil Slater
Neil Slater

Reputation: 27207

The trick required here is to assign a unique group instead of nil. You can do that simply by creating a generic Ruby Object.

orig = [
  {"timestamp"=>1347119549, "category"=>nil}, 
  {"timestamp"=>1347119547, "category"=>"Monkeys"}, 
  {"timestamp"=>1347119543, "category"=>nil}, 
  {"timestamp"=>1347119542, "category"=>"Monkeys"}]

# The "tricky bit"
grouped = orig.group_by { |x| x["category"] ?  x["category"] : Object.new  }

# Sort the siblings within the groups (note negation causes reverse order)
grouped.values.each { |siblings| siblings.sort_by! { |a| -a["timestamp"] } }

# Sort the list by first (i.e. "best" sort order) timestamp in each group 
sorted_groups = grouped.sort_by { |group_id,siblings| -siblings.first["timestamp"] }

# Remove group ids and flatten the list:
result = sorted_groups.map { |group_id,siblings| siblings }.flatten
=>  [
 {"timestamp"=>1347119549, "category"=>nil}, 
 {"timestamp"=>1347119547, "category"=>"Monkeys"}, 
 {"timestamp"=>1347119542, "category"=>"Monkeys"}, 
 {"timestamp"=>1347119543, "category"=>nil}
]

Upvotes: 1

Arup Rakshit
Arup Rakshit

Reputation: 118289

require 'pp'

ar = [
    {"timestamp" => 1347119549, "category" => nil},
    {"timestamp" => 1347119547, "category" => "Monkeys"},
    {"timestamp" => 1347119543, "category" => nil},
    {"timestamp" => 1347119542, "category" => "Monkeys"}
]

pp ar.group_by{|h| h['category'] ? h['category'] : h['timestamp']}.
   map{|k,v| v.sort_by{|h| -h['timestamp']}}.
   sort_by{|a| -a[0]['timestamp']}.flatten
# >> [{"timestamp"=>1347119549, "category"=>nil},
# >>  {"timestamp"=>1347119547, "category"=>"Monkeys"},
# >>  {"timestamp"=>1347119542, "category"=>"Monkeys"},
# >>  {"timestamp"=>1347119543, "category"=>nil}]

require 'pp'

a = [
  {"timestamp"=>1347119549, "category"=>nil},
  {"timestamp"=>1347119547, "category"=>"Monkeys"},
  {"timestamp"=>1347119543, "category"=>nil},
  {"timestamp"=>1347119542, "category"=>"Monkeys"},
  {"timestamp"=>1347119548, "category"=>"Dog"},
  {"timestamp"=>1347119544, "category"=>"Dog"}
]

pp a.group_by{|h| h['category'] ? h['category'] : h['timestamp']}.
   map{|k,v| v.sort_by{|h| -h['timestamp']}}.
   sort_by{|a| -a[0]['timestamp']}.flatten 
# >> [{"timestamp"=>1347119549, "category"=>nil},
# >>  {"timestamp"=>1347119548, "category"=>"Dog"},
# >>  {"timestamp"=>1347119544, "category"=>"Dog"},
# >>  {"timestamp"=>1347119547, "category"=>"Monkeys"},
# >>  {"timestamp"=>1347119542, "category"=>"Monkeys"},
# >>  {"timestamp"=>1347119543, "category"=>nil}]

Upvotes: 2

Related Questions