Reputation: 681
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
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
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
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
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