`String#truncate` の `:separator` と過長 `:omission` のバグを修正
ActiveSupport の String#truncate が :separator と :omission を同時に指定した際、omission が truncate_to 以上の長さになると期待と異なる文字列が返っていた問題を、条件分岐の追加で解消しました。
背景
このバグは String#truncate が :separator オプションを利用したときに顕在化します。truncate_to から omission の長さを引いた length_with_room_for_omission が負になると、内部で rindex に負の開始位置が渡され、Ruby はそれを文字列末尾からのオフセットとして解釈します。その結果、stop が文字列末尾付近に設定され、self[0, stop] がほぼ全体を返すため、omission だけが返るべきケースで元文字列が長く出力されました。
技術的な変更
修正は length_with_room_for_omission が 0 以下のときは即座に omission を返す という一行のガードを truncate メソッド冒頭に追加するだけです。これにより、separator ブランチは負の長さの場合に入り込まず、非 separator パスと同等の振る舞いになります。
変更前:
def truncate(truncate_to, options = {})
omission = options[:omission] || "..."
length_with_room_for_omission = truncate_to - omission.length
stop = if options[:separator]
rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
else
length_with_room_for_omission
end
# ...
end
変更後:
def truncate(truncate_to, options = {})
omission = options[:omission] || "..."
length_with_room_for_omission = truncate_to - omission.length
return omission.dup if length_with_room_for_omission <= 0
stop = if options[:separator]
rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
else
length_with_room_for_omission
end
# ...
end
テストも test_truncate_with_separator_and_omission_longer_than_truncate_to を追加し、truncate_to が omission より小さいケースで separator が文字列または正規表現のいずれでも正しく省略文字だけが返ることを確認しています。
def test_truncate_with_separator_and_omission_longer_than_truncate_to
assert_equal "[...]", "Hello Big World!".truncate(2, omission: "[...]", separator: " ")
assert_equal "[...]", "Hello Big World!".truncate(2, omission: "[...]", separator: /\s/)
end
設計判断
separator ブランチに入る条件を length_with_room_for_omission >= 0 に限定した点が中心の設計判断です。ガードを追加するだけで既存コードへの侵入を最小限に抑え、String#truncate の従来の API 互換性を保持しています。負の余裕がある場合は文字列本体を切り取る余地が無いため、即座に omission を返すのが自然な振る舞いであり、今回の変更はその直感に沿った実装です。
まとめ
String#truncate の separator と過長 omission の組み合わせ で生じていた不整合を、1 行の条件チェックで解消しました。既存の非 separator 動作はそのまま保たれ、テストカバレッジも追加されたことで回帰リスクが低減しています。