Rubyでカレンダー問題をやってみた



Railsを絶賛勉強中の初心者です。railsを学び初めて1ヶ月ほどになりました。
改めてRubyの学習を１からしたいと考えている中、Rubyプログラマの伊藤さんが書いている以下の記事に辿り着きました。


[アウトプットのネタに困ったらこれ！？Ruby初心者向けのプログラミング問題を集めてみた（全10問）](https://blog.jnito.com/entry/2014/06/06/104420)

今日はこちらを参考にさせていただき、自分の書いたコードとその中で学んだことについて書いていきます。
今後も自分なりにブラッシュアップをしていきたいと思っていますが、一度他の方に見てもらいご指摘も受けれたら幸いです。

8/29 追記
改めて自分で重複している部分などを書き直してみました。

<h2>カレンダー問題　自分の回答</h2>
まずは、作った自分のプログラムは以下の通りです。


```calender.rb

require  'Date'

#今月の最終日を確認するメソッド
def last_day(year,month)
  if month == 2 && Date.valid_date?(year,month,29)
    return 29
  elsif month == 2
    return 28
  elsif Date.valid_date?(year,month,31)
    return 31
  else
    return 30
  end
end

#今月が何週かを確認する
def how_many_weeks(day_array)
  days_count = day_array.length
  if days_count / 7 == 5 && days_count % 7 != 0
    return 6
  elsif days_count / 7 == 5
    return 5
  elsif days_count / 7 == 4 && days_count % 7 != 0
    return 5
  else
    return 4
  end
end

#今日の日付を生成する
today = Date.today

#今日の日付から西暦/月/月の英字名を出力する
year = today.year
month = today.month
name_of_month = today.strftime("%B")

#今月の開始曜日を調べる
day_of_the_week = Date.new(year,month,1).wday


#最終日を基準に日付の配列を作成する
last_day = last_day(year,month)
day_array = [*1..last_day]

#開始曜日毎に配列に空欄を追加する
day_of_the_week.times do
  day_array.unshift(" ")
end

#今月が何週かを確認する
weeks_count = how_many_weeks(day_array)
#カレンダータイトルの作成
calender_title = name_of_month + " " + year.to_s


#カレンダーの出力
puts calender_title.center(21," ")
printf("%3s%3s%3s%3s%3s%3s%3s\n","Su","Mo","Tu","We","Th","Fr","Sa","Su")
weeks_count.times do
  #1週分の内容weekにセットする
  week = day_array.shift(7)

  #nilの場合の対処として、空欄を入れる
  if week.length < 7
    e  = week[6]
    week.map {|e| e ? e : " " }
  end

  #1週分を出力する
 printf("%3s%3s%3s%3s%3s%3s%3s\n",
        week[0],week[1],week[2],week[3],week[4],week[5],week[6])
end


```

8/29 追記
新たに書き直したコードは以下の通りです。
最初よりだいぶわかりやすく書けました・・・！

```calender.rb

require  'Date'

#今日の日付を生成する
today = Date.today

#今日の日付から西暦/月を出力する
year = today.year
month = today.month

#今月の開始曜日を調べる
dweek = Date.new(year,month,1).wday

#最終日を基準に日付の配列を作成する
last_day = Date.new(year,month,-1).day
day_array = [*1..last_day]

#開始曜日毎に配列に空欄を追加する
dweek.times do
  day_array.unshift(" ")
end

#今月が何週かを確認する
weeks_count = day_array.length / 7
weeks_count += 1 if day_array.length % 7 > 0

#カレンダータイトルの作成
calender_title = Date.new(year,month,1).strftime("%B %Y")
calender_head = ["Su","Mo","Tu","We","Th","Fr","Sa"]

#カレンダーの出力
puts calender_title.center(21," ")
puts " " + calender_head.join(" ")

#ある週分timesを回す
weeks_count.times do
  #1週分の内容weekにセットする
  week = day_array.shift(7)

  #nilの場合の対処として、空欄を入れる
  if week[6] == nil
    week.map {|e| e ? e : " " }
  end

  #1週分を出力する
  printf("%3s%3s%3s%3s%3s%3s%3s\n",
        week[0],week[1],week[2],week[3],week[4],week[5],week[6])
end

```


今月だと出力結果は以下の通りになります

```出力結果(コマンドライン)

     August 2020     
 Su Mo Tu We Th Fr Sa
                    1
  2  3  4  5  6  7  8
  9 10 11 12 13 14 15
 16 17 18 19 20 21 22
 23 24 25 26 27 28 29
 30 31   
```

<h2>作成中に学んだrubyの機能</h2>
<h3>・printf</h3>
C言語に由来する、表示形式をフォーマット指定子で指定した上でprintを行う機能。カレンダーを作成する上で、どうやって形を整えるのかを考えていた際にこちらを見つけました。
今回は、ヘッダ部分の曜日と中身の日付の出力に使っています。

```
printf("%3s%3s%3s%3s%3s%3s%3s\n","Su","Mo","Tu","We","Th","Fr","Sa","Su")
```
`%s`で文字列を出力を示し
その間の`3`で表示桁数を指定
最後の`\n`で改行をしています。

ヘッダ部と日付を同じフォーマット指定をすることで綺麗に整えることができました。

参考：[標準入力関数](http://www9.plala.or.jp/sgwr-t/c/sec05.html)



<h3>・String#center</h3>
上記のprintfだとタイトルのセンタリングができないと気付き、rubyで文字列をセンタリングする方法を調べていた中で見つけたメソッドです。

```
puts calender_title.center(21," ")
```
ここでは、カレンダー部の大きさである21の範囲で、文字が中央にくるように" "(空欄)を入れてもらう処理をしています。便利ですね！

参考：[String#center Rubyリファレンス](https://docs.ruby-lang.org/ja/latest/method/String/i/center.html)

<h3>・nilを空欄にする mapメソッド</h3>
カレンダーは、最初の日の曜日に応じて冒頭と末尾に空欄を入れる必要があります。
冒頭については、日付を持つ`day_array`に、1日の曜日に応じて配列に空欄を追加することで対処したのですが、末尾の空欄については、うまく対処する方法が分からず付け焼き刃かもしれませんが以下の方法にしました。

```
  #1週分の内容weekにセットする
  week = day_array.shift(7)

  #nilの場合の対処として、空欄を入れる
  if week.length < 7
    e  = week[6]
    week.map {|e| e ? e : " " }
  end
```
`if week.length < 7`
その週の日付を持つ`week`の配列が7つ以下しか要素をもってない場合に、
`e  = week[6]`
week[6]を呼び出すことで、値がない部分に全てnilを入れる。
`week.map {|e| e ? e : " " }`
そこからmapで三項演算式(nilの場合に" "(空欄)に置き換える)をおこない、nilを置換する。

もう少しいい方法があると思うのですが...一度作り切りたかったためこのような対処としました。

参考：[ruby でエラーにならないよう array や value の nil をどうにかする](https://www.d-wood.com/blog/2014/05/13_6210.html)

（8/29 追記）
<h3>・Dateクラスでのマイナス値</h3>
前回記載した時は、カレンダーを描画する対象の日付（当月）の最終日をメソッドで作成していいたのですが、Dateクラスで簡単に確認できることをしりました。

`last_day = Date.new(year,month,-1).day`
日付に-1を入れることで最終日を生成できます

<h3>・当月に何個週があるか確認する</h3>
これはもう少し賢い方法がある気がするのですが、、、以前はメソッドで確認していた部分をぎゅっと2行にしました。

```
#今月が何週かを確認する
weeks_count = day_array.length / 7
weeks_count += 1 if day_array.length % 7 > 0
```

<h2>最後に</h2>

ひとまずは、色々試行錯誤しつつ非常に楽しく作ることができました。
冒頭のブログでも書かれている通り、作っただけで終わらず、もっと考えて綺麗なコードにしていくことが重要だと思います。
まずはこのカレンダー作成アプリのコードをもう一度見直し、テストを書いてみるところからはじめようと思います。

改めてですが、参考にさせていただいた各サイト様、ありがとうございました！