48

I have an array of objects that I need to sort by a position attribute that could be an integer or nil, and I need the objects that have the nil position to be at the end of the array. Now, I can force the position to return some value rather than nil so that the array.sort doesn't fail, but if I use 0 as this default, then it puts those objects at the front of the sort. What's the best way to to do this sort? should I just set the nil values to some ridiculously high number that is 'almost' always guaranteed to be at the end? or is there some other way i could cause the array.sort method to put the nil attribute objects at the end of the array? the code looks like this:

class Parent
  def sorted_children
     children.sort{|a, b| a.position <=> b.position}
  end
end

class Child
  def position
    category ? category.position : #what should the else be??
  end
end

now, if i make the 'else' something like 1000000000, then it's most likely gonna put them at the end of the array, but I don't like this solution as it's arbitrary

7 Answers 7

122

I would just tweak your sort to put nil items last. Try something like this.

foo = [nil, -3, 100, 4, 6, nil, 4, nil, 23]

foo.sort { |a,b| a && b ? a <=> b : a ? -1 : 1 }

=> [-3, 4, 4, 6, 23, 100, nil, nil, nil]

That says: if a and b are both non-nil sort them normally but if one of them is nil, return a status that sorts that one larger.

Sign up to request clarification or add additional context in comments.

2 Comments

Agreed, this should be the accepted answer. If you are happy to rely on truthy and falsy values, you can get it a fraction shorter: foo.sort { |a,b| a <=> b || (b && 1) || -1 }
I didn't want to override the spaceship (!) operator on the whole model, just in a particular operation so this is much better for my need.
20

I handle these kinds of things like this:

 children.sort_by {|child| [child.position ? 0 : 1,child.position || 0]}

5 Comments

Can you explain this a bit more? It seems like you're using 0 as a magic number when position is nil, which may not be valid since position isn't defined to be greater 0 (it's a reasonable assumption, but isn't necessarily the case).
No, the 0 at the end is just to avoid calling <=> on nil. The first element of the array already insures that all the nils will come after all the valid positions. The second element subsorts among the things with positions. You could put something in there to subsort the ones with nils, but the example didn't call for anything, so I just used 0. It could be 42 or "wombat", it doesn't matter.
Ah, ok, the confusion was due to my lack of Ruby knowledge. Am I understanding correctly in that you're actually creating an array/list of two values, 1 and the value for if the value is nil... or 0 and the value if the value is non-nil... then sorting using that pair as "the value to sort on"? Nifty idea, it just wasn't clear to me because I didn't recognize the syntax.
Right, I'm representing each child by a two-element subarray: [0,position] if the position is not nil, and [1,0] if the position is nil. Ruby's comparison of arrays compares the keys in order, so all the subarrays that start with 0 (the non-nil positions) sort first, subsorted by position. Then all the subarrays with 1 (the nil positions). Technically these nils are then subsorted by those "0"s, which is why 0 could be 42 or "pie" or whatever. If you had another criteria by which to subsort the nils, it'd go there instead. Can't just leave nil there, because it doesn't have a <=> method.
At least in Ruby 1.9.3p327, nil <=> nil returns 0. Using spaceship between nil and anything else generally returns nil, but nil and itself returns 0. As a result, [1, nil, 2] <=> [1, nil, 3] returns -1. As a result, as long as there is a value earlier in the array (i.e. the 0 / 1 for non-nil vs. nil) that divides the nil values from the non-nil values, then it should be safe to use the nil.
19

How about in Child defining <=> to be based on category.position if category exists, and sorting items without a category as always greater than those with a category?

class Child
  # Not strictly necessary, but will define other comparisons based on <=>
  include Comparable   
  def <=> other
    return 0 if !category && !other.category
    return 1 if !category
    return -1 if !other.category
    category.position <=> other.category.position
  end
end

Then in Parent you can just call children.sort.

2 Comments

I'd be somewhat iffy on this since it limits your ability to sort on anything else without being inconsistent. That being said, if position is the natural sort order for parent, then it makes a reasonable amount of sense to do it this way.
@RHSeeger If you check the edit history, I had originally added a caveat about what to do if you need to sort by different criteria, but it was confusingly worded and I didn't think it added much. Basically, you can add other comparison methods that are named whatever you want, and if you need to sort on them, do it in a block like the original example.
6

To be fair, I'm not very familiar with Ruby, so take this as more of an algorithm idea rather than a code one... and rewrite the ?: operator as whatever Ruby has that's cleaner.

Can't you just check for nil in the comparison:

class Parent
  def sorted_children
     children.sort{|a,b|( a and b ) ? a <=> b : ( a ? -1 : 1 ) }
  end
end

Edited to use Glenra's code, which implements the same thing as mine but in a smaller (and probably easier to read) amount of code.

Comments

4

The most simple solution for me is

def sorted_children(children)
  children.sort_by { |child| child.position || -1}
end

2 Comments

This only works if the array has positive integers.
children.sort_by { |child| child.string_fields || "z"}. Not sure I'd use it though
1

I haven't done Ruby in a while, but you could split the null-checking from the sorting (and just allow Child#position to return null):

def sorted_children
  children.reject{|c| c.position.nil?}.sort_by(&:position) +
    children.select{|c| c.position.nil?}
end

Admittedly it's not the most efficient solution, but it doesn't have any magic numbers.

3 Comments

".nil?" is superfluous since c.position is true iff c.position is not nil, but you could leave it in for readability. I like it!
I suppose that's true in this case, since "position" is probably a number, but since Ruby has 2 "false" values (nil and false), I try not to assume that non-nil means true. I've gotten bitten by that before. :-)
children.reject{|c| c.position.nil?} can be replaced with children.compact docs.ruby-lang.org/en/3.1/Enumerable.html#method-i-compact
1

You can do this without overriding the spaceship operator by defining a new comparison method.

class Child
  include Comparable   
  def compare_by_category(other)
    return 0 if !category && !other.category
    return 1 if !category
    return -1 if !other.category
    category.position <=> other.category.position
  end
end

The sort method can take a block, so you can then sort using this new method:

children.sort {|a,b| a.compare_by_category(b) }

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.