Rubyでカレンダーを表示するプログラムを作成する

## はじめに
以前からRubyの学習を進めており、アウトプットとしてカレンダーを出力するプログラムを作成しました。

今回は、プログラムの作成の過程についてまとめていきます。私のようなプログラミング学習初心者の一助となれば幸いです。

## アウトプットイメージ

アウトプットイメージは、以下の添付画像の通り
Macのターミナルで`cal`コマンドを実行した結果と同じものを作成します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/50d4faaa-5f4d-f2c7-455c-7600d460fc15.png)

追加要件として、以下も満たすことにします。

- 曜日は月曜日始まりにする（`cal`コマンドの実行結果は日曜日始まり）
- `ruby`コマンドでファイルを実行する際、`-m`オプションで月を指定できるようにする
    - `-m`オプションで指定した月のカレンダーを出力する（今年）
- `-m`オプションの引数を指定しない場合、今月のカレンダーを表示する（今年）
- `-m`オプションの引数が不正な月の場合、次のエラーを出力する

``` plaintext
➜ ruby calendar.rb -m 22
22 is neither a month number (1..12) nor a name
```

なお、今回は以下を利用してプログラムを作成しています。

- [class Date](https://docs.ruby-lang.org/ja/latest/class/Date.html#S_TODAY)
- [library optparse (Ruby 3.1 リファレンスマニュアル)](https://docs.ruby-lang.org/ja/latest/library/optparse.html#optionarg)

## 作業手順

すべての内容を一度に考えるのではなく、下記の通り3ステップに分けて考えていくことにしました。

1. 月と年
1. 曜日
1. 日付

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/a380ae62-4daa-5591-401f-96f0747c8855.png)



## 1. 月と年

カレンダーを出力する関数を定義し、引数には年と月をもたせます。
ただ、月は英語で表示させる必要があります。

``` ruby:calender.rb
require 'date'

def show_calender(year, month)
  date = Date.new(year, month)
  puts "    #{date.strftime('%B')} #{year}    "
end

show_calender(2024, 6)
```

期待する結果を出力できました。

![スクリーンショット 2024-06-30 12.54.08.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/47aca911-bfc4-b0d2-9667-784d2ca14bb0.png)


## 2. 曜日

曜日は、月曜日始まりにして出力します。

``` ruby:calender.rb
require 'date'

def show_calender(year, month)
  date = Date.new(year, month)
  puts "    #{date.strftime('%B')} #{year}    "
  puts 'Mo Tu We Th Fr Sa Su'
end

show_calender(2024, 6)
```

出力結果が、少しカレンダーらしくなりました。

![スクリーンショット 2024-06-30 12.51.33.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/6ec8c0b8-c476-2efc-096b-0a630e48767e.png)

## 3.日付

最後は、日付部分になります。プログラムを作成するにあたり、さらに課題を分解して考えます。

1. 出力するカレンダーの月によって、日付の開始曜日が異なる
1. 日付は日曜日で改行する必要がある

### 出力するカレンダーの月によって、日付の開始曜日が異なる

[Date#wday](https://docs.ruby-lang.org/ja/latest/class/Date.html#I_WDAY)を利用すれば、解決できそうです。

具体的には、以下の通りです。

1. 当該月のカレンダーの開始日付から`wday`を利用して開始曜日を数字（0から6）で取得する
2. wdayの出力結果ごとに、スペースの挿入回数を調整して開始曜日の直下から開始日付を出力する

念のため、表で整理しておきます。

|開始曜日  |wdayの出力結果  |スペースの挿入回数  |
|---|---|---|
|月  |1  |0  |
|火  |2  |1  |
|水  |3  |2  |
|木  |4  |3  |
|金  |5  |4  |
|土  |6  |5  |
|日  |0  |6  |

コードにすると、以下の通りとなります。

``` ruby:calender.rb
require 'date'

def show_calender(year, month)
  date = Date.new(year, month)
  puts "    #{date.strftime('%B')} #{year}    "
  puts 'Mo Tu We Th Fr Sa Su'

  if date == 0
    print '   ' * 6
  else
    print '   ' * (date.wday - 1)
  end
  print "#{date.day.to_s.rjust(2)}"
end

show_calender(2024, 6)
```

2024/6/1は、土曜日なので開始曜日の直下に開始日付が出力できています。

![スクリーンショット 2024-06-30 13.27.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/0ea0d312-d24a-3fd3-8eeb-0147ed4cfb7a.png)

補足となりますが、`rjust`メソッドを利用して日付を出力する際に右寄せしています。

また、`rjust`メソッドは、Stringクラスのメソッドのため`date.day`の戻り値（Integer）を`to_s`メソッドでStringに変換する必要があります。

### 日付は日曜日で改行する必要がある

上記課題を取り組む前に、先に当該月の日付をすべて出力しておきます。

日付の出力は、以下の2ステップで行います。

1. 当該月の開始日と終了日の日付を取得する
2. 1.の日付範囲を繰り返し処理で出力する

当該月の開始日と終了日の取得に伴い、全体的にコードを書き直します。

``` ruby:calender.rb
require 'date'

def show_calender(year, month)
  start_date = Date.new(year, month)
  end_date = Date.new(year, month, -1)
  puts "    #{start_date.strftime('%B')} #{year}    "
  puts 'Mo Tu We Th Fr Sa Su'

  if start_date == 0
    print '   ' * 6
  else
    print '   ' * (start_date.wday - 1)
  end

  (start_date..end_date).each do |date|
    print "#{date.day.to_s.rjust(2)}"
  end
end

show_calender(2024, 6)
```

6月の日付がすべて出力されました、あともう少しです。

![スクリーンショット 2024-06-30 13.53.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/0d530eef-8927-e2ad-ef6e-d7ddf53569d4.png)

Dateクラスには、日曜日かどうかを判定する便利なメソッド（```sunday?```）が用意されているので、以下の1行を追加して日曜日で改行します。

``` ruby
puts if date.sunday?
```

アウトプットイメージと同じカレンダーが出力できました。

![スクリーンショット 2024-06-30 14.00.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/34e24a5f-c1e9-a1b9-5439-89144f9f792a.png)

※本プログラムの作成には、`puts`メソッドと`print`メソッドの違いを把握しておくことが必要です。

|メソッド名  |出力後の改行  |配列の表示  |文字列変換  |戻り値  |
|---|---|---|---|---|
|puts  |あり  |要素ごとに改行  |to_s  |nil  |
|print  |なし  |改行しない  |to_s  |nil  |

引用：[プロを目指す人のためのRuby入門[改訂2版] 言語仕様からテスト駆動開発・デバッグ技法まで (Software Design plus)](https://www.amazon.co.jp/%E3%83%97%E3%83%AD%E3%82%92%E7%9B%AE%E6%8C%87%E3%81%99%E4%BA%BA%E3%81%AE%E3%81%9F%E3%82%81%E3%81%AERuby%E5%85%A5%E9%96%80-%E6%94%B9%E8%A8%822%E7%89%88-%E8%A8%80%E8%AA%9E%E4%BB%95%E6%A7%98%E3%81%8B%E3%82%89%E3%83%86%E3%82%B9%E3%83%88%E9%A7%86%E5%8B%95%E9%96%8B%E7%99%BA%E3%83%BB%E3%83%87%E3%83%90%E3%83%83%E3%82%B0%E6%8A%80%E6%B3%95%E3%81%BE%E3%81%A7-Software-Design/dp/4297124378/ref=sr_1_2_sspa?__mk_ja_JP=%E3%82%AB%E3%82%BF%E3%82%AB%E3%83%8A&crid=2QOIZK1A9QF3Q&dib=eyJ2IjoiMSJ9.2LfTLqfglMz6c1N_gAH2KIu5RnH0zLD_-RYWo6EdQpuakdkq13d6EOoLqNRP3iFI8AEwE6BEEroU8muu3yebS7iIv8wapdrI1YzOeaNdJXvEAPeOZEQrz88RNcpi0ZlUfJ2uQ74O6g9JZerngAsFKk9i2uIwuMUYHivDWuxi1ZQuvUpX3o8Jwu26pQUQr-G3_3AaUtqFoMhKvwHO144ghhvhwS0nR8fqR_QWozR_wXQsI53VYx5U0I42Xy3Sip5xY5iUkyKrPdKWtT78RK7hYR_2kOhkWnphL4-rMeprMQA.PkhmosY2MB0yuGVyOSpuqM3d70IyFElSUSAI57oCEB8&dib_tag=se&keywords=ruby&qid=1719724264&sprefix=ruby%2Caps%2C303&sr=8-2-spons&sp_csd=d2lkZ2V0TmFtZT1zcF9hdGY&psc=1)

## -mオプションについて

次に、`ruby`コマンドで先ほどの`calender.rb`ファイルをターミナルで実行する際、`-m`オプションで月を指定できるようにします。

例えば、以下のようなコマンドを実行すると、2024年5月のカレンダーを出力します。

``` plaintext
ruby calendar.rb -m 5
```

他の要件も忘れずに実装していきます。

- `-m`オプションの引数を指定しない場合、今月のカレンダーを表示する（今年）
- `-m`オプションの引数が不正な月の場合、次のエラーを出力する

## library optparseとは

`-m`オプションの実装には、library optparseを利用します。

library optparseについて、[公式ドキュメント](https://docs.ruby-lang.org/ja/latest/library/optparse.html#optionarg)の内容を理解していきます。

optparseを利用する流れは、基本的に以下のようです。

1. OptionParser オブジェクト opt を生成する
1. オプションを取り扱うブロックを opt に登録する
1. opt.parse(ARGV) でコマンドラインを実際に parse（解析） する

:::note warn
3.の後に、2.のブロックの内容が実行される
:::

``` ruby:sample.rb
require 'optparse'
opt = OptionParser.new

opt.on('-a') {|v| p v }
opt.on('-b') {|v| p v }

opt.parse!(ARGV)
p ARGV

```

上記コードの実行結果は、以下の通りです。

```plaintext
ruby sample.rb -a foo bar -b baz
# => true
     true
     ["foo", "bar", "baz"]
```

ARGVは、argument vectorの略でオプション（`-a`, `-b`）とオプションの引数の値を配列に格納しています。

ARGVからオプションを取り除くには、`opt.parse!(ARGV)`と破壊的メソッドを利用すれば問題ないようです。

また、`OptionParser#on`メソッドのオプション定義で末尾に引数名（`VAL`）を書くと、オプションは引数を受け付けることの指定となります。

``` ruby:sample.rb
require 'optparse'
opt = OptionParser.new

opt.on('-a VAL') {|v| p v }         # <- " VAL" を追加

opt.parse!(ARGV)
p ARGV
```

実行結果は以下の通りです。ただ、`-a`だけでなく`VAL`もオプションとして解析されているのか、ARGV配列は空になっています。

```
ruby sample.rb -a hoge

# => "hoge"
     []
```

## -mオプションを実装する
それでは、`-m`オプションを実装していきます。

簡潔に言うと、`-m`オプションの引数の値を`show_calender`関数の第二引数に渡す実装を行います。

具体的には、optparse利用時の最後のブロックの内容を実行するタイミングで

- ハッシュに、`-m`オプションの値を格納する

ができれば、`show_calender`関数の第二引数にハッシュの値を渡すだけです。

:::note warn
値の受け渡しには、ハッシュを利用していますが他に良い方法があるかもしれません。
:::

以下のコードを追加すれば、要件に沿ったプログラムの完成です。

``` ruby:calender.rb
require 'optparse'
opt = OptionParser.new

values = {}

values[:year] = Date.today.year
values[:month] = Date.today.month

opt.on('-m month') { |month| values[:month] = month.to_i }
opt.parse!(ARGV)

if values[:month] < 1 || values[:month] > 12
  puts "#{values[:month]} is neither a month number (1..12) nor a name"
else
  show_calender(values[:year], values[:month])
end
```
`ruby calender.rb -m 5`とターミナルで実行すれば、2024年5月のカレンダーが出力されました。

![スクリーンショット 2024-06-30 16.11.09.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/00986521-ab40-eb86-0ed2-ceaf52b42b11.png)

オプションなしでコマンドを実行すれば、コマンド実行時の月のカレンダーが出力されます。

![スクリーンショット 2024-06-30 16.15.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/eaaf56d6-857d-7f86-4ab1-199759d58fe9.png)

1から12以外の数字をコマンドのオプション引数に指定すると、エラーも出力されるようになっています。

![スクリーンショット 2024-06-30 16.18.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/390c9323-6bb0-eed3-17f2-5cb75221fe62.png)

## おわりに

初めて1からプログラムを作成しましたが、かなり大変でした。ただ、エラーを解決し期待する結果が出力できた瞬間は楽しかったです。

学びとしては、課題を細かく分解して一つずつ時間をかけて考えていくことが大切だと感じました。

引き続きプログラミングの学習を続けていきます。

## 参考
https://docs.ruby-lang.org/ja/latest/class/Date.html

https://docs.ruby-lang.org/ja/latest/library/optparse.html#optionarg