Enumerable#in_order_of が filter:false で nil を除去しないよう修正
Enumerable#in_order_of の filter: false オプションは、シリーズに未定義の要素を結果に残すことが仕様とされていますが、以前の実装では compact が誤って適用され、nil が除去されていました。本稿ではその原因と修正内容を詳しく解説します。
背景
Enumerable#in_order_of は、キー関数と順序配列(series)を受け取り、要素を指定順に並べ替えるユーティリティです。ドキュメントには次のように記載されています。「filter オプションが false に設定されている場合、シリーズに含まれない要素は結果に残す」(quoted). この期待は、nil 要素も含めて保持されることを意味します。
しかし実装の filter: false ブランチで compact が呼び出されており、nil が意図せず除去されていました。具体例として、[3, nil, 1, 2].in_order_of(:itself, [1, 2, 3], filter: false) は [1, 2, 3] という結果になり、nil が失われていました。この挙動は filter: true 用のコードをコピーしたことが原因です。
結果として、API 仕様と実装が食い違い、利用側が nil を保持できないというバグが発生していました。ドキュメントどおりに動作させるための修正が必要でした。
技術的な変更
filter: false ブランチから余計な compact 呼び出しを削除しました。変更前後のコードは以下の通りです。
# 変更前
sort_by { |v| series.index(v.public_send(key)) || series.size }.compact
# 変更後
sort_by { |v| series.index(v.public_send(key)) || series.size }
このシンプルな差分により、nil 要素がそのままソート結果に残ります。機能的な変更はなく、既存の filter: true ロジックにも影響を与えません。
また、回帰テストを追加し、nil が保持されることを自動的に検証できるようにしました。テストコードは次のとおりです。
def test_in_order_of_with_filter_false_preserves_nil_elements
values = [3, nil, 1, 2]
assert_equal [1, 2, 3, nil], values.in_order_of(:itself, [1, 2, 3], filter: false)
end
さらに、CHANGELOG に「Enumerable#in_order_of の filter: false が nil を除去していたバグを修正」旨のエントリを追加し、利用者に変更点を周知できるようにしています。
この修正は 1 行の削除とテスト追加のみであり、既存の挙動に副作用を与えることはありません。filter: true のロジックはそのまま残ります。
設計判断
filter: true と filter: false の二つの分岐はそれぞれ異なる目的を持つため、共通化しすぎない設計が採られています。filter: true では group_by の結果に nil が生じるため compact が必要ですが、filter: false では sort_by が nil を生成しないので compact は不要です。今回の修正は、分岐ごとのロジックを正しく保ち、意図しない副作用を除去するという設計方針を再確認したものです。
この判断により、API の契約(contract)と実装の整合性が保たれ、将来的な機能追加時にも各分岐の責務が明確なまま保守しやすくなります。
まとめ
Enumerable#in_order_of の filter: false オプションが nil を除去していたバグを、余計な compact 呼び出しの削除で修正しました。追加した回帰テストにより、今後同様のミスが再発しにくくなっています。ドキュメント通りの動作が保証されたことで、利用者は安心して filter: false を活用できるようになりました。