学習日記

学習記録

lsコマンドを作ってみる

Rubyでlsコマンドを作ります

表題の通りlsコマンドを作ってみます とはいっても完全再現するととんでもないコード量になるので、今回はオプションを実装しません

また今回はフィヨルドブートキャンプのプラクティス内にあるlsコマンドを作るを題材として作っていきますので フィヨルドブートキャンプ生の方でプラクティスを修了前の方は閲覧を控えるようお願いします。

また筆者はフィヨルドブートキャンプ生ですが、該当プラクティス合格前に書いたコードになります。

余談になりますが、フィヨルドブートキャンプのカリキュラムではファイルやフォルダを指定するようにすることは必須ではありません。

要件

今回模倣するlsコマンドの要件です

  1. 引数無しで実行するとカレントディレクトリ内のファイルが一覧表示されること
  2. Rubyの標準ライブラリしか使わないこと
  3. 最大3列を維持して表示すること
  4. 列数の変更に対応できること
  5. ファイルやディレクトリを指定できること
  6. ファイルが指定された場合、相対パスで指定された場合相対パスを、チルダパスで指定された場合絶対パスを、絶対パスで指定された場合絶対パスを返す。
  7. 2バイト文字があっても表示が崩れないこと

実装

繰り返しになりますが、フィヨルドブートキャンプ生の方でプラクティス修了前の方は閲覧を控えるようお願い致します。

手始めに使う予定のライブラリと定数を定義します、合わせてshebangも記述しておきます。

#!/usr/bin/env ruby
# frozen_string_literal: true

require 'pathname'
DISPLAY_COLUMNS_COUNT = 3
DISPLAY_WIDTH = 18

mainメソッドを定義して、その中に動きを定義していく感じで書いていこうと思います

ついでに後で使う予定の配列も定義しておきます

def main
  argument_type = argument_parse if ARGV[0] # ARGVには引数が入っている
  directory_files = [] # 後の計算で使用
end

処理を開始する前に今回はファイルやディレクトリを指定可能にしなければならないので、引数が渡されてるのかどうか、また引数はファイルなのかフォルダなのかを判断します。

ではその判断してくれるargument_parseの中身を書いていきます

def argument_parse(argument)
  if File.directory?(argument)
    :directory_path
  elsif File.file?(argument)
    :file_path
  else
    :invalid
  end
end

こんな感じでいけるかな?

rubyではメソッドが最終的に評価した式が返り値になるので、argument_typeに引数に渡されたタイプ毎に分類されたシンボルが返ります この時文字列ではなくシンボルを返すのは内部的にシンボルのほうが処理速度が速いからです。

次にmainメソッド側にこの戻り値を利用した処理を書き加えていきます

def main
  argument_type = argument_parse if ARGV[0]
  case argument_type
  when :invalid
    error_message # 渡された引数が不正ならエラーメッセージを表示するメソッドを呼び出す
  when :file_path
    display_file_results(ARGV[0]) # 渡された引数がファイルなら結果を出力するメソッドを呼び出す
  when :directory_path
    directory_files = Dir.glob("#{ARGV[0]}/*").map(&File.method(:basename)) # 渡された引数がディレクトリなら、渡されたディレクトリの一覧を取得する
  else
    directory_files = Dir.glob('*') # 引数が渡されなかったら現在のカレントディレクトリ内の一覧を取得する
  end
end

こんな感じでいいでしょう。

実装が簡単な引数が不正な値だった場合から片付けていきます

def error_message
  puts "'#{ARGV[0]}'にアクセスできません:そのようなファイルやディレクトリはありません"
end

いい感じですね。

では次に引数がファイルだった場合を考えていきます。

前述の要件の通りファイルが指定された場合は少し面倒くさいです。

何故この仕様なのかというと、筆者の環境でlsコマンドを使うとそのように結果が返ってくるからです。

相対パス絶対パスでファイルを指定される時というのはそもそもそのままの値を返せばよく、チルダパスを指定された時のみ絶対値を計算すればよさそうなので、このように・・・

def display_file_results(file_path)
  specified_path = file_path[0] == '~' ? Pathname.new(file_path).expand_path('~') : file_path # 渡されたパスの先頭が"~"なら絶対パスを、それ以外ならそのまま返す
  puts specified_path
end

実装完了です。

pathnameは標準ライブラリですので、requireを忘れないようにしてください。

次はいよいよ本命のディレクトリに移っていきましょう。

1個のメソッドに処理を詰め込んでしまってもいいのですが、後からのメンテナンス性と可読性を考えて小分けに実装することにします

メインメソッドをこんな感じに追記して...

def main
省略

  row_size = calculate_row_size(directory_files) # 表示する行のサイズを計算するメソッド
  display_data = create_display_data(row_size,directory_files) # 計算した行のサイズを元に1行毎の配列を作る
  display_directory_results(row_size, display_data) # 1行毎の配列を列数に合わせて表示する
end

この3個の処理を経て結果が表示されるようにしていきます。

まずはcalulate_row_sizeで行数を計算します。

def calculate_row_size(directory_files)
  (directory_files.size + DISPLAY_COLUMNS_COUNT - 1) / DISPLAY_COLUMNS_COUNT - 1 #最適な行数の計算
end

表示したい列の行数をこれで row_sizeに格納したので

次はこれをもとに実際に表示するためのデータを配列に入れていきます

def create_display_data(row_size, directory_files)
  directory_files.each_slice(row_size + 1).to_a # 1行毎の配列を作る,3行目は自動的に余りの数で作ってくれる
end

これで1行当たりのデータが配列になってdirectory_filesに格納されました

次は先ほど計算したrow_sizeと今回作成した配列directory_filesを使って実際にデータを出力します

def display_directory_results(row_size, display_data)
  (row_size + 1).times do |row|
    DISPLAY_COLUMNS_COUNT.times do |col|
      print display_data[col][row].to_s.ljust(DISPLAY_WIDTH) #ファイル名を18文字として左寄せで表示
    end
    puts
  end
  puts
end

これで完成です、お疲れ様でし・・

そうです、このままだと2バイト文字に対応してません。

2バイト文字が混ざると表記がずれるのは、2バイト文字であっても1文字は1文字とカウントされるためです。

そこで今回は表示するファイルに含まれる2バイトの文字を数え、その分だけDISPLAY_WIDTHから値を引くことで表記ずれを防ごうと思います。

まずは表示する側にコードを書きます。

def display_directory_results(row_size, display_data)
  (row_size + 1).times do |row|
    DISPLAY_COLUMNS_COUNT.times do |col|
      wide_chars_count = count_characters(display_data[col][row]) || 0 # wide_chars_countに2バイト文字の数を入れる、2バイト文字がなかった場合nilが返るので対策しておく
      print display_data[col][row].to_s.ljust(DISPLAY_WIDTH - wide_chars_count)
    end
    puts
  end
  puts
end

それでは次にチェック側を書きます

def count_characters(file_name)
  file_name.each_char.count { |char| char.bytesize > 1 } if !!(file_name =~ /[^[:ascii:]]/) #2バイト文字があればカウントを1増やす
end

if !!(file_name =~ /[^[:ascii:]]/)はなくても動作しますが、むやみやたらにファイル名の全文字を解析するのもリソースの無駄なので、ascii文字だけで構成されたファイル名の場合はスルーさせています。

これで後は表示を確認してあげれば

大丈夫そうですね、お疲れ様でした。

Rubyで素数判定するプログラムを作成

Ruby素数を判定するプログラムを作ってみる

今日はRubyを使って、素数を判定するプログラムを作ってみようと思います。

要件として

  • 素数はその数値と1以外の数値で割ることのできない数とする
  • 例外として1は素数としないものとする
  • 正数であればどのような数でも素数か判定出来る
  • 異常な値はこないものとする
  • Minitestを使いテストコードも一緒に作成する必要がある

とします。

今回のケースでは先にテストコードを作ったほうが楽そうなので、テストコードから作っていきます。

ではまずどのようなテストケースを作成するかを考えていきます。

今回は簡単に7が正しく素数と判断されるか,10が正しく素数ではないと判断されるか,1を正しく素数ではないと判断できるか

ではテストコードを書いていきます

require 'minitest/autorun'
require '../lib/prime.rb'

class TestClass < Minitest::Test
  def test_prime
    assert_output(stdout = "7は素数です\n" , stderr = nil) { prime?(7) }
    assert_output(stdout = "10は素数ではありません\n" , stderr = nil) { prime?(10) }
    assert_output(stdout = "1は素数に含めません\n" , stderr = nil) { prime?(1) }
  end
end

今回はこのように書いてみました。 もし一緒に作ってみる方がいれば、require先の実行ファイルはご自身の環境に合わせてください。

assert_outputstdoutに期待される標準出力stderrに期待される標準エラー出力を入力します。 ブロックに渡した式が期待されている出力かどうかを評価します。 つまりprime?(x)が実行された時にstdoutと同じ値が返ってくればテストをパスします

stdoutに1を指定された際は、1は自身と1で割り切れるが例外として素数としない数値なので違うメッセージを返すことにします。

それでは実際にプログラムを作っていきます

どのようなプログラムが必要でしょうか?

一旦書き出していこうと思います

  • どの値が素数なのか判断するのに引数が必要
  • 素数かどうかを判断する
  • 素数であれば"xは素数です"と出力する
  • 素数でない場合は"xは素数ではありません"と出力する
  • 1の時は別枠で処理を用意して、"1は素数に含めません"と出力する

簡単に実装できそうなところだけ書いてみます

def prime?(num)
    if num == 1
    puts "1は素数に含めません"
    return
    end
end

とりあえず引数が1だった場合は長々と処理させるのも無駄なので、さっさと処理を抜ける実装にしました。 それでは本格的に実装していきます。

どのようにすれば素数かどうかを判断できるでしょうか? 7をケースに考えてみます。

素数の定義は" 素数はその数値と1以外の数値で割ることのできない数とする"としたので

その数値以上の値であれば絶対に割り切れないことは自明なので7の場合であれば

2,3,4,5,6で割り切れるかどうかを確認していけば素数であるかどうかを判断できそうです。

いくつかその値を出力する方法は考えられますが、今回はdowntoメソッドを使って実装してみます。

(num -1).downto(2) とすれば6から2まで値を1ずつ減らしながら繰り返すという意味を持ちますので、期待通りの動作をしそうです。

ここから持ってきた値を7と割っていけばいいので

def prime?(num)
    if num == 1
    puts "1は素数に含めません"
    return
    end
    (num - 1).downto(2) do |number|
      if num / number # が割り切れる
        puts "#{num}は素数ではありません"
      end
    end
  end

とすればよさそうですね。

割り切れるかどうかの判断ですが、ここは簡単に%演算子で解決できそうです。

後は割り切れなかった場合に素数である旨を出力させれば

def prime?(num)
    if num == 1
    puts "1は素数に含めません"
    return
    end
    (num - 1).downto(2) do |number|
      if num % number == 0
        puts "#{num}は素数ではありません"
      end
    end
    puts "#{num}は素数です"
end

完成!でしょうか?

このままだと最後に絶対に素数である旨が出力されてしまいますね。

素数ではないことが分かったらもうあとは処理をしなくていいので、処理を抜けさせてしまいましょう

def prime?(num)
    if num == 1
    puts "1は素数に含めません"
    return
    end
    (num - 1).downto(2) do |number|
      if num % number == 0
        puts "#{num}は素数ではありません"
        return
      end
    end
    puts "#{num}は素数です"
end

returnを付けることで、メソッドを抜けることが出来ます。

それではテストしてみます

Run options: --seed 23997

# Running:

.

Finished in 0.001881s, 531.6109 runs/s, 1594.8328 assertions/s.

1 runs, 3 assertions, 0 failures, 0 errors, 0 skips

問題なさそうですね。

リファクタリングの余地はありそうですが、今回はこれで完成とします。

お疲れ様でした。