sujay
sujay

Reputation: 681

Custom sort array of hashes based on multiple key/value pairs

I have an array of hashes which I need to sort based on two different key value pairs.

Here is the array I am trying to sort:

array_group =  [
 {operator: OR, name: "some string", status: false},
 {operator: AND, name: "other string", status: false},
 {operator: _NOT_PRESENT, name: "another string", status: true},
 {operator: AND, name: "just string", status: true}
]

I want to sort array_group so I have items with status: true first, followed by status: false, followed by the items with operator: _NOT_PRESENT and finally sort it based on name, resulting in something like:

array_group = [
 {operator: AND, name: "just string", status: true},
 {operator: AND, name: "other string", status: false},
 {operator: OR, name: "some string", status: false},
 {operator: _NOT_PRESENT, name: "another string", status: true},
]

Is there a way that I can get this done without creating sub-arrays and sorting them and concatenating them back?

Upvotes: 6

Views: 2524

Answers (4)

sschmeck
sschmeck

Reputation: 7685

You can also use Enumerable#sort_by. The example builds an array which is compared element by element when sorting.

array_group.sort_by { |e| [e[:operator] == "_NOT_PRESENT" ? 1 : 0, 
                           e[:status] ? 0 : 1, 
                           e[:name]] }

The example above orders records with operator: "_NOT_PRESENT" also by :status. The following snippet precisely performs the ordering from the question.

def priority(h)
  case
  when h[:operator] == "_NOT_PRESENT" then 3
  when h[:status] == false            then 2
  # h[:status] == true
  else 1
  end
end

array_group.sort_by { |e| [priority(e), e[:name]] }

Upvotes: 6

Cary Swoveland
Cary Swoveland

Reputation: 110675

I assume the unspecified constants OR, AND and NOT_PRESENT are defined as follows (for example):

OR = "or"
AND = "and"
NOT_PRESENT = "not present"

(Constants must begin with a capital letter. _NOT_PRESENT is a local variable.)

When using Enumerable#sort_by, you need to sort on an array containing three elements, corresponding to :status, :operator and :name, in that order. The first of those three elements is smallest (say 0) if the value of :status is true, next smallest (say 1) if the value of :status is false and largest (say 2) if it is anything else. We have:

def rearrange(arr)
  arr.sort_by do |h|
    [
      h[:status]==true ? 0 : (h[:status]==false ? 1 : 2),
      (h[:operator]==NOT_PRESENT) ? 0 : 1, 
      h[:name]
    ]
  end
end

array_group = [
 {operator: AND, name: "just string", status: true},
 {operator: OR, name: "some string", status: false},
 {operator: AND, name: "other string", status: false},
 {operator: NOT_PRESENT, name: "another string", status: true},
]

rearrange(array_group)
  #=> [{:operator=>"not present", :name=>"another string", :status=>true},
  #    {:operator=>"and", :name=>"just string", :status=>true},
  #    {:operator=>"and", :name=>"other string", :status=>false}, 
  #    {:operator=>"or", :name=>"some string", :status=>false}] 

This is not the result desired, but it is consistent with my understanding of the question.

Upvotes: 0

Doguita
Doguita

Reputation: 15703

You can use Enumerable#sort_by using an array to keep the groups you want and sorting by name without lose those groups. For example:

[[0,0,1], [1,1,1], [0,1,0], [1,0,0], [0,1,1]].sort_by &:itself
# => [[0, 0, 1], [0, 1, 0], [0, 1, 1], [1, 0, 0], [1, 1, 1]]

Using this, you can set an array with length 2. Using index 0 to "group" by the elements you want and index 1 to sort by name.

OR = "OR"
AND = "AND"
_NOT_PRESENT = "_NOT_PRESENT"

array_group =  [
 { operator: OR, name: "z string", status: false },
 { operator: AND, name: "a string", status: false },
 { operator: AND, name: "z string", status: true },
 { operator: OR, name: "a string", status: true },
 { operator: _NOT_PRESENT, name: "d string", status: true },
 { operator: _NOT_PRESENT, name: "b string", status: true },
 { operator: _NOT_PRESENT, name: "c string", status: false },
 { operator: _NOT_PRESENT, name: "a string", status: false }
]

# Types of "groups" you want to keep. Greater values will be at the end
BY_NOT_PRESENT = 2
BY_STATUS_FALSE = 1
BY_STATUS_TRUE = 0

array_group.sort_by do |a|
  group = if a[:operator] == _NOT_PRESENT
            BY_NOT_PRESENT
          else
            a[:status] ? BY_STATUS_TRUE : BY_STATUS_FALSE
          end
  [group, a[:name]]
end
#=> [{:operator=>"OR", :name=>"a string", :status=>true},
#     {:operator=>"AND", :name=>"z string", :status=>true},
#     {:operator=>"AND", :name=>"a string", :status=>false}, 
#     {:operator=>"OR", :name=>"z string", :status=>false}, 
#     {:operator=>"_NOT_PRESENT", :name=>"a string", :status=>false}, 
#     {:operator=>"_NOT_PRESENT", :name=>"b string", :status=>true}, 
#     {:operator=>"_NOT_PRESENT", :name=>"c string", :status=>false}, 
#     {:operator=>"_NOT_PRESENT", :name=>"d string", :status=>true}]

I am doing this way, because if just sort_by an array like [status, not_present, name] then status will have precedence on sort, overriding the name sort, resulting this:

#=> [{:operator=>"OR", :name=>"a string", :status=>true}, 
#    {:operator=>"AND", :name=>"z string", :status=>true}, 
#    {:operator=>"AND", :name=>"a string", :status=>false}, 
#    {:operator=>"OR", :name=>"z string", :status=>false}, 
#    {:operator=>"_NOT_PRESENT", :name=>"b string", :status=>true}, 
#    {:operator=>"_NOT_PRESENT", :name=>"d string", :status=>true}, 
#    {:operator=>"_NOT_PRESENT", :name=>"a string", :status=>false}, 
#    {:operator=>"_NOT_PRESENT", :name=>"c string", :status=>false}]

Upvotes: 0

Dmitry Sokurenko
Dmitry Sokurenko

Reputation: 6132

You can use the Array.sort method. It accepts a block with two arguments (x, y), when x is larger than y it should return 1, otherwise -1, and 0 if they are equal.

The code:

OR = "OR"
AND = "AND"
_NOT_PRESENT = "_NOT_PRESENT"

array_group =  [
 {operator: OR, name: "some string", status: false},
 {operator: AND, name: "other string", status: true},
 {operator: _NOT_PRESENT, name: "another string", status: true},
 {operator: AND, name: "just string", status: true}
]

results = array_group.sort do |x, y|
  next x[:operator] == _NOT_PRESENT ? 1 : -1 if x[:operator] == _NOT_PRESENT || y[:operator] == _NOT_PRESENT
  next x[:status] ? -1 : 1 if x[:status] != y[:status]
  next x[:name] <=> y[:name]
end

And btw, you your input and output data doesn't match each other — the hash with OR is false in the input, but true in the output.

I believe your output should look like:

[{:operator=>"AND", :name=>"just string", :status=>true},
 {:operator=>"AND", :name=>"other string", :status=>true},
 {:operator=>"OR", :name=>"some string", :status=>false},
 {:operator=>"_NOT_PRESENT", :name=>"another string", :status=>true}]

That output will actually match your sorting logic.

Upvotes: 1

Related Questions