自転車とプログラミング

自転車メーカーに勤める会社員がプログラミングを学ぶ中で感じたことを書きます。最近サービス作りました。

人のプログラムを読解しよう カレンダープログラム編02

フィヨルドブートキャンプに所属しているYukiです。 今回はフィヨルドブートキャンプで課された「ターミナルでカレンダーを出力しよう」という課題を終了し、他の受講生の解答が見られるようになりましたので、その読解に取り組みます。 今回からフィヨルドブートキャンプのメンターが作ったプログラムの読解です。

クラスメソッド

  def self.generate(year: Date.today.year, month: Date.today.month)
#クラスメソッドgenerateを生成し、キーワード引数として日付オブジェクトの今日の年を変数yearに、今日の月を変数monthに代入し、デフォルト値としている。

このメソッド自体はgenerateの名前通り、カレンダーを構成するメソッドを呼び出して、ターミナル上に生成する役目を持っている。名付けがうまい。

メソッド名の冒頭にselfとつくか、class << self ~ endでくくるとそのメソッドはクラスメソッドになる。

普通にメソッドを定義した場合はインスタンスメソッドになる。インスタンスメソッドはそのクラスのインスタンスに対して呼び出せるメソッドであり、インスタンスに含まれるデータ(メソッド.new(引数)みたいな)を読み書きする場合はインスタンスメソッドを定義する。 なので、インスタンスに含まれるデータを読み書きしない場合はクラスメソッドを使う、となる。 途中の()内はインスタンス変数を生成しており、それを参照するプログラムが走る想定。

この部分ではクラス外からコマンドライン引数を引っ張ってきているのでクラスメソッドが使われている。

クラスのprivate

今回のプログラムはクラス内部がpublicとprivateに区切られていた。その復習。

Rubyのメソッド公開レベルにはpublic,protected,privateの3つがある。 publicはクラス外部からでも呼び出せるメソッドのこと。今回はカレンダーをターミナルに出力するメソッドとカレンダー構成するメソッドの2つがpublicとなっていた。カレンダーを構成するメソッドはこれ単体では呼び出せないのでprivateでもいいような気がするけれど…。

privateはレシーバを指定して呼び出すことが出来ないメソッドのこと。なので基本的にクラス外部から呼び出すことは出来ない。こちらにはカレンダーの日付をカレンダー通りに空白や改行を付けるメソッドがprivateだった。

protectedは今回使われていないし初見なのでざっと学習。protectedはそのメソッドを定義したクラスとそのサブクラスのインスタンスメソッドからレシーバ付きで呼び出せる。レシーバ付きでというのがprivateとの違い。インスタンスを作ってそれに関する処理をさせることができる。

日付の生成

  def body(year, month)
    #月の初日と最終日を取得
    省略

    blanks = Array.new(first_date.wday)
    full_days = [*blanks, *(1..last_date.day)]
    format = -> (n) { n.to_s.rjust(2, ' ') }
    full_days.each_slice(7).map do |days|
      days.map(&format).join(' ')
    end
  end
#月初と月末の日付を取得。変数blanksに初日の曜日数(0〜6)を配列で代入。変数full_daysに配列blanksと日にちの範囲オブジェクトを展開して代入。変数formatに手続きオブジェクトを代入。変数full_daysの値を7個ずつmapメソッドを呼び出す。7個ずつの値はブロック変数に代入されて変数format+joinメソッドで各値が右詰め2桁+空白で配列として返される。

変数blanksに代入されたのはなにか

変数blanksに代入されたのはその月初日の曜日数(wday)に応じた数のnilを値とする配列オブジェクト。wdayメソッドは日〜土で0〜6の値を返す。 Array.newメソッドは配列を生成するメソッドで引数に配列の個数と値をとる。個数のみを取る場合はnilを生成すると思われる。

このカレンダープログラムは連続した数値としての日付を土曜日区切りで改行して出力するやり方と、7日区切りで出力するやり方がある。 日付を連続した数値として捉える場合は、初日の前に曜日に応じた空白を入れ込む形になってわりと力技になりやすい(とおもう。自分もこっち)。 7日区切りの場合は初日を迎えるまでの空白も日付のように捉えて、空白を取得して日付と同じ処理をすることになる。表示される日付だけではなく空白も日付のように捉える発想なのでよりプログラマー的手法だと思う。

splat展開

配列を展開して複数の引数として渡す場合に使われる手法。

たとえばblanksをそのまま渡していた場合はwdayメソッドで得られた数の分だけのnilを有する配列が代入される。

アロー演算子とProcオブジェクト

-> (引数) {処理内容}でProcオブジェクトを生成できる。 Procオブジェクトは手続きや処理内容といったブロックをオブジェクトにしたもののこと。

アロー演算子で生成したProcオブジェクトを変数に代入して、あとで&変数の形で使うことでブロックと認識させて処理をしている。

コマンドラインオプションの取得とカレンダー出力メソッドの実行

if $0 == __FILE__
#コマンドラインの取得
    省略

  puts Cal.generate(**params)
end
#もしこのプログラムのスクリプト名とソースファイル名が同じであるときは、コマンドラインオプションを取得し変数paramsに代入、カレンダー出力メソッドを出力する。

if $0 == __FILE__の意図

初見でよくわからなかった部分。

$0は現在実行中の Ruby スクリプトの名前を表す文字列。 __FILE__は現在のソースファイル名を表す疑似変数。 どちらも相対パス

で、ファイルを直接実行された場合は両者とも同じ値、ファイル名を表すことになる。この場合は普通にif内の処理が実行される。 このファイルがライブラリとして使用される場合は両者の値は異なる。処理は実行されない。

この表記自体はテストコードやサンプルに使用されるそうで、誤爆とかを防ぐ役割なのかと想像。

ハッシュの展開

コマンドラインから得たオプションは、それぞれキーyearに対する値キーmonthに対する値という形でハッシュparamsに代入されている。

**はハッシュの展開を意味する記号で、反映するとputs Cal.generate({year: 値, month:値)となっている。 これによって当記事冒頭のself.generate(year: Date.today.year, month: Date.today.month)コマンドラインオプションがあった場合は、そのハッシュが引き渡されることになる。コマンドラインオプションがない場合はデフォルトのDate~メソッドの値となる。

以上

メンターのプログラムはさすがレベルが高いですね。 私のプログラムの半分の行数で同じことを実現しているので、簡略化のテクニックや考えが満載でした。