はじめに
こんにちは!アメリカの大学で語学を学びながら、独学でソフトウェアエンジニアを目指している者です。
現在、Rubyの Time
や Date
クラスについて学習しており、練習問題に取り組む中で、以下のようなミスをしてしまいました。今回は、「よくあるミス」ではなく、私が実際に犯した誤りを振り返り、同じ過ちを繰り返さないための学びを共有したいと思います。
やりたかったこと
UNIXの ls -t
コマンドのように、ファイルを更新日時が新しい順に表示するRubyスクリプトを作成しようとしていました。
ls -t
は「ファイル名」と「更新日時」を基に並べ替えるため、Rubyでも同様にファイル一覧の取得、更新日時の取得、並び替え、表示というステップを踏む必要があります。
間違ったコード
以下が、私が書いた間違ったコードです:
hash = Hash.new(0)
def ls_t(path)
Dir.glob(["#{path}/**/*","#{path}/**/.*"]) do |name|
unless File.directory?(name)
hash[name] = File.mtime(name)
end
end
hash.values.sort.each do |c|
printf("%s \n", hash[c])
end
end
このコードの問題点
1. 変数スコープの問題
hash
を ls_t
メソッド外で定義しているため、ls_t
内で呼び出すことができません。
2. hash.values.sort
後の不適切な参照
hash.values.sort
は更新日時 (Time
オブジェクト) のみを抽出した配列を返します。しかし、その後の処理で:
hash.values.sort.each do |c|
printf("%s \n", hash[c])
end
このコードでは、c
は更新日時であり、hash
のキーはファイル名なので、hash[c]
でファイル名を取得することはできません。
3. 並べ替えロジック不足
ls -t
のような機能を再現するには、ファイル名と更新日時のペアを維持しながら並べ替える必要があります。値だけ抜き出して並べ替えてしまうと、どの更新日時がどのファイル名に対応するのかがわからなくなります。
4. Hash.new(0)
ではなく {}
で初期化すべき理由
Hash.new(0)
は「存在しないキーにアクセスしたとき、デフォルトで0を返す」ハッシュを生成します。しかし、このケースではキーは必ずファイル名として存在する前提なので、そんなデフォルト値は不要です。{}
で初期化する通常のハッシュを使えば、存在しないキーにアクセスすると nil
が返るため、不適切なアクセスに気づきやすくなります。また、Hash.new(0)
だと「キーが無いのに0が返る」ことでデバッグが難しくなる場合があります。
問題を解決したコード
以下が修正後のコードです:
def ls_t(path)
hash = {}
# ファイルの更新日時を取得して hash に格納
Dir.glob(["#{path}/**/*", "#{path}/**/.*"]) do |name|
unless File.directory?(name)
hash[name] = File.mtime(name)
end
end
# 更新日時で並べ替えて新しいファイルから表示
sorted = hash.sort_by { |_, mtime| mtime }.reverse
sorted.each do |filename, _|
puts filename
end
end
修正ポイント
-
hash
をメソッド内で{}
で初期化し、毎回クリーンな状態で処理を開始できるようにしました。 -
sort_by { |_, mtime| mtime }
の|_, mtime|
はブロック引数のパターンマッチで、「キー(ファイル名)は使わない(変数_
で受ける)」という意味です。_
は変数名として無視を表す慣用的記法で、mtime
だけを利用して並べ替えます。 -
並べ替え後の
each do |filename, _|
では、逆にファイル名だけが必要で、更新日時は使わないためfilename, _
と書いて_
で更新日時を受け取っています。これも「更新日時は使わない」という意図を明示するためです。
(追記) Pathnameクラスを使用した場合のコード
コメントで補足いただきましたが、今回のケースのようなpath
を扱う場合はPathname
クラスを用いることでかなり簡単にコードが書けるようです。
Pathname
を使用する場合はrequire "pathname"
を記述する必要があります
require 'pathname'
def ls_t(path)
dir = Pathname.new(path)
# ファイルと更新日時を取得し、並べ替えて新しい順に表示
dir.glob("**/{*,.*}")
.reject(&:directory?)
.sort_by(&:mtime)
.reverse
.each { |file| puts file }
end
まとめ
今回の振り返りで得られたポイントは以下の通りです。
-
スコープ管理:
hash
や他の変数はメソッド内で初期化することで、呼び出しごとの状態管理が容易になります。 -
関連情報を保持したまま並べ替え:ファイル名と更新日時を保持したハッシュを
(キー, 値)
のままsort_by
してからreverse
を行うことで、ls -t
のような挙動を実現できます。 -
不要なデフォルト値を避ける:
Hash.new(0)
のようなデフォルト値つきハッシュは、このケースでは不要です。{}
にしておけば、存在しないキーへのアクセスが明確にnil
になり、問題発生時に気づきやすくなります。 -
|_, mtime|
や|filename, _|
について:これはブロック引数のパターンマッチ(分割代入)で、2要素の配列[キー, 値]
を(キー, 値)
という形で受け取るとき、一方を使わないことを明示できます。_
は使わない変数であることを示す慣習的な記号で、コードの可読性と意図の明示に役立ちます。
この学びを活かして、次回からはより意図通りに動作するコードを書けるようにしていきたいと思います!