nilを含むソートと、Enumerable#partitionメソッド

sortやsort_byの対象にnilが含まれていると扱いにくい

例えば、以下のような例。

  • ユーザーを最終ログイン時刻でソートする
class User
  attr_accessor :name,:last_login
  
  def initialize(name,last_login)
    @name = name
    @last_login = last_login
  end
  
  def to_s
    "#{self.name}:#{self.last_login}"
  end
end

users = []
users << User.new("bob",Time.mktime(2007,1,1)) <<
         User.new("jon",Time.mktime(2007,5,6)) <<
         User.new("ben",Time.mktime(2008,4,1))

users.sort_by{|user| user.last_login}
puts users

#結果----------------------
bob:Mon Jan 01 00:00:00 +0900 2007
jon:Sun May 06 00:00:00 +0900 2007
ben:Tue Apr 01 00:00:00 +0900 2008


この場合は良いのですが、最終ログインがnilのユーザーが含まれていると面倒。

  • 最終ログインがnilのユーザーが含まれている場合
users = []
users << User.new("bob",Time.mktime(2007,1,1)) <<
         User.new("jon",Time.mktime(2007,5,6)) <<
         User.new("ben",Time.mktime(2008,4,1)) <<
         User.new("kai",nil) <<
         User.new("rai",nil)

users.sort_by{|user| user.last_login}
puts users

#結果----------------------
array_partition_test.rb:21:in `sort_by': comparison of Time with nil failed (ArgumentError)
from array_partition_test.rb:21


ソート対象にnilが含まれているので、内部で、Time.mktime(2008,4,1) <=> nilというような状態が発生し、nilとTimeを比較しようとしてエラーになる。

解決策

Enumerable#partitionを使う

各要素に対してブロックを評価した値が真であった要素からなる配列と偽であった要素からなる配列からなる配列を返します。

#Userにメソッド追加
class User 
  def logged_in?
    !self.last_login.nil?
  end
end

users = []
users << User.new("bob",Time.mktime(2007,1,1)) <<
         User.new("jon",Time.mktime(2007,5,6)) <<
         User.new("ben",Time.mktime(2008,4,1)) <<
         User.new("kai",nil) <<
         User.new("rai",nil)

#last_loginがあるないでユーザーを分割
logged_in_users, non_logged_in_users = 
  users.partition{|user| user.logged_in?}

#last_loginがあるユーザーだけソートして、ないユーザーとの和をとったものをusersに代入
users = (non_logged_in_users + logged_in_users.sort_by{|user| user.last_login})
puts user

#結果----------------------
kai:
rai:
bob:Mon Jan 01 00:00:00 +0900 2007
jon:Sun May 06 00:00:00 +0900 2007
ben:Tue Apr 01 00:00:00 +0900 2008


もう少し綺麗にならないものかな