zenet_logo

-株式会社ゼネット技術ブログ-

Rubyの"Enumerable#any?"と、"Enumerable#all?"の挙動で少しハマってしまった話

お疲れさまです。システム事業部の坂本です。
普段はRuby on Railsを利用したシステム開発を行っています。

今回はRails開発の中で、以前少しハマってしまった挙動があるため
備忘録を兼ねて皆さんにお伝え出来ればと思います。


■はじめに:Enumerable#any?Enumerable#all?

まず、RubyにおけるEnumerable#all?は、配列に対する条件が
全ての要素が真ならtrueを、何か一つでも偽になるならfalseを返すメソッドです。

一方、RubyにおけるEnumerable#any?は、配列に対する条件が
何か一つでも真ならtrueを、全て偽になるならfalseを返すメソッドです。

そのため、以下のような挙動になります。

[1, 7, -3].all? { |n| n > 0 } => false
[1, 7, -3].any? { |n| n > 0 } => true

[1, 7, 14].all? { |n| n > 0 } => true
[1, 7, 14].any? { |n| n > 0 } => true

[-1, -7, -14].all? { |n| n > 0 } => false
[-1, -7, -14].any? { |n| n > 0 } => false

ここまでは明快なのですが、空の配列に対しては以下の通りになります。

[].all? { |n| n > 0 } => true
[].any? { |n| n > 0 } => false

それぞれのメソッドで、trueを返すのかfalseを返すのかが異なります。
この仕様が原因で、以下のようなソースを書いた際に少しハマってしまいました。


■状況

例えば、以下のようなUserモデルがあったと仮定します。

class User < ApplicationRecord
  enum status: {
    active: '稼働中',
    withdrawn: '退会済'
  }

  # 最近登録したユーザか?
  # return [Boolean]
  def new_user?
    created_at + 1.week > Date.today
  end
end

その状況で、以下のような処理を行いたいと考えました。

  • Userをとある条件で絞り込んで、全員User#new_user?がtrueになれば処理Aを実行する。

  • 誰か1人でもUser#new_user?を満たさない場合は、処理Bを実行する。

そして、以下のようなソースを書きました。

class UserDataMaintenanceController
  class << self
    def execute

      ## 省略

      # もし、conditionsの条件で検索したUserが全員最近登録したユーザだったら
      if User.where(conditions).all?(&:new_user?)
        # 処理A
      else
        # 処理B
      end
    end
  end
end

■上記が原因でハマってしまったこと

私は当初、UserDataMaintenanceController.executeを実行した際、
以下の通りに実行されるだろうと考えていました。

  • 【パターン1】もし、conditionsの条件で検索したUserが、全員最近登録したユーザだったら
    処理Aを実行する。

  • 【パターン2】もし、conditionsの条件で検索したUserに、1人でも最近登録したユーザじゃないユーザがいれば
    処理Bを実行する。

  • 【パターン3】そもそも、conditionsの条件で検索したUserが誰もいなければ
    処理Bを実行する。

しかし、前述でもお伝えした内容からお察しの通り、
パターン3は以下の通りになります。

User.where(...).all?(&:new_user?)

# ↓ User.where(...)の結果が、[]だった場合

[].all?(&:new_user?) # => true

そのため、conditionsの条件で検索したUserが誰もいなかった場合に実行されるのは、
当初想定していた処理Bではなく、処理Aでした。
そして私は当初、この仕様に気づかず数時間程「何故動かないのだろう……?」と頭を悩ませていました……。

暫く調べた後に、前述のEnumerable#any?Enumerable#all?の挙動が違うことに気付き、
以下のように改修することで解決出来ました。

class UserDataMaintenanceController
  class << self
    def execute

      ## 省略
      
      users = User.where(conditions)    
      # もし、conditionsの条件で検索したUserが存在し、全員が最近登録したユーザだったら
      if users.present? && users.all?(&:new_user?)
        # 処理A
      else
        # 処理B
      end
    end
  end
end

こうすると、【パターン3】の「そもそも、conditionsの条件で検索したUserが誰もいなければ処理Bを実行する。」が実現出来ました。


■まとめ

Enumerable#any?Enumerable#all?を使用する際は、
メソッド実行対象の配列は、どんな時でも必ず要素が1個以上ある配列になるのか?
を確認した上で使うようにしましょう。

この辺をしっかり確認せずに使うと、
今回お伝えしたような内容でハマってしまう原因になりかねません。

以上、ありがとうございました。