0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Ruby】 今日書いたコードの振り返り#2

Last updated at Posted at 2024-12-07

はじめに

こんにちは!アメリカの大学で語学を学びながら、独学でソフトウェアエンジニアを目指している者です。

現在、Rubyの TimeDate クラスについて学習しており、練習問題に取り組む中で、以下のようなミスをしてしまいました。今回は、「よくあるミス」ではなく、私が実際に犯した誤りを振り返り、同じ過ちを繰り返さないための学びを共有したいと思います。

やりたかったこと

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. 変数スコープの問題

hashls_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

修正ポイント

  1. hash をメソッド内で {} で初期化し、毎回クリーンな状態で処理を開始できるようにしました。

  2. sort_by { |_, mtime| mtime }|_, mtime| はブロック引数のパターンマッチで、「キー(ファイル名)は使わない(変数 _ で受ける)」という意味です。_ は変数名として無視を表す慣用的記法で、mtime だけを利用して並べ替えます。

  3. 並べ替え後の 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

まとめ

今回の振り返りで得られたポイントは以下の通りです。

  1. スコープ管理hash や他の変数はメソッド内で初期化することで、呼び出しごとの状態管理が容易になります。

  2. 関連情報を保持したまま並べ替え:ファイル名と更新日時を保持したハッシュを (キー, 値) のまま sort_by してから reverse を行うことで、ls -t のような挙動を実現できます。

  3. 不要なデフォルト値を避けるHash.new(0) のようなデフォルト値つきハッシュは、このケースでは不要です。{} にしておけば、存在しないキーへのアクセスが明確に nil になり、問題発生時に気づきやすくなります。

  4. |_, mtime||filename, _| について:これはブロック引数のパターンマッチ(分割代入)で、2要素の配列 [キー, 値](キー, 値) という形で受け取るとき、一方を使わないことを明示できます。_ は使わない変数であることを示す慣習的な記号で、コードの可読性と意図の明示に役立ちます。

この学びを活かして、次回からはより意図通りに動作するコードを書けるようにしていきたいと思います!

0
0
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?