6

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?

10
  • The statement of your question suggests that status can have a value other than True or False. Is that correct? Commented Nov 6, 2015 at 21:52
  • yes status is not actually a boolean its a string which I am later wrapping to boolean values if it has true or false in some other part of code Commented Nov 6, 2015 at 21:54
  • 1
    How did "other string"'s status switch from true to false? Why is "another string" not first in the sorted array, consider that it ties with "other string" on the value of :status and is the only one of the two to have _NOT_PRESENT as the value of :operator? Commented Nov 6, 2015 at 22:12
  • 1
    In that case you need to edit the question and correct the statement of the desired ordering. You should leave the current statement and then add something like "Edit: this statement is not correct ect. It should read:...". If you merely replace the current statement it would render comments and answers meaningless and puzzling. Also correct your desired result (array). Commented Nov 7, 2015 at 23:20
  • 1
    And while those things might have been done, we'll waste our time suggesting things you've already tried and rejected unless you tell us. Help us help you by reducing the possible things to try. Commented Nov 7, 2015 at 23:23

4 Answers 4

6

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]] }
Sign up to request clarification or add additional context in comments.

10 Comments

If embedded in a method this will not work because _NOT_PRESENT is a local variable. If you change _NOT_PRESENT to NOT_PRESENT, both here and in array_group, after having defined the constant NOT_PRESENT, you get {:operator=>"and", :name=>"just string", :status=>true} as the first element of the sorted array, which is incorrect.
@CarySwoveland The first element you mention is the first element of the desired result. What's wrong with it?
Your first line puts "another string" after all other hashes (regardless of the value of :status, which is the first filter). Suppose :status=true for "another string" (as it is now), but :status=false for all other hashes. Then "another string" should be sorted first, not last.
@CarySwoveland I understand the question in another way. 1st place ordering is [status: true, status: false, operator: _NOT_DEFINED], 2nd is sorted by :name. That's what the desired result looks like.
@sschmeck you are right about the order of array items i.e., status: true comes first (all status true items are further sorted by name) then comes status false (all status false items further sorted by name) and then finally status _NOT_PRESENT will be present :) this answer works for me but I am trying few more scenarios before I accept it ,,, thank you for your answer
|
1

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.

2 Comments

thank you for the answer,,, sorry for presenting wrong info,,, it was a typo regarding one entry but my final output should always have _NOT_PRESENT at the last..
I've added another version, so _NOT_PRESENTs are always last, otherwise status == true goes before status == false, when statuses are equal then names are compared alphabetically.
0

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}]

Comments

0

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.

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.