views:

1212

answers:

6

Hi All,

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

+1  A: 

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.

Ken
".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!
glenra
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. :-)
Ken
+3  A: 

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.

RHSeeger
+8  A: 

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

The in Parent you can just call children.sort.

Brian Campbell
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
@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.
Brian Campbell
+3  A: 

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 and b ) ? a <=> b : ( a ? -1 : 1 ) }

RESULT=> [-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.

glenra
+1  A: 

I handle these kinds of things like this:

 children.sort_by {|child| [child.position ? 0 : 1,child.position || 0]}
glenn mcdonald
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).
RHSeeger
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.
glenn mcdonald
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.
RHSeeger
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.
glenn mcdonald
A: 

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.

Actually, that could be a fine solution too. Ruby makes it pretty easy to sort and rearrange sets and parts of sets. So suppose we do that - store zero instead of nil, sort it, then move the zero items from the front to the end of the array. Many, many ways to do that. Here's one example:

foo=[2,0,43,45,0,5,0]

lastz=foo.sort!.rindex(0)          # sort, then find the rightmost "0"
foo=foo[lastz+1..-1]+foo[0..lastz] # rearrange the array with "0" at end

RESULT=>[2, 5, 43, 45, 0, 0, 0]
glenra