【Ruby のまずいコード】2022 年の日曜日はいくつ？

# お題

2022 年は日曜日がいくつあるか数えてください。

# コード

## コード 1

```rb
require "date"

count = 0
(1..12).each do |month|
  (1..31).each do |day|
    begin
      date = Date.new(2022, month, day)
    rescue Date::Error
      next
    end
    count += 1 if date.wday == 0
  end
end

puts count
```

## コード 2

```rb
require "date"

count = 0
(Date.new(2022, 1, 1)..Date.new(2022, 12, 31)).each do |date|
  count += 1 if date.sunday?
end

puts count
```

# 講評

どちらのコードも，2022 年一年間のすべての日付を Date オブジェクトとして生成し，それが日曜日である場合にカウンターを 1 増やしています。
違いは，すべての日付を生成するやり方と，日曜日かどうかを判定するやり方ですね。

## コード 1

コード 1 では，月（1〜12）と日（1〜31）の二重ループを使って全ての日付を得ようとしています。
しかし，月によって末日は異なるので，その扱いが問題になります。
[Date.new](https://docs.ruby-lang.org/ja/3.0.0/method/Date/s/civil.html) は，ありえない年・月・日の組み合わせを与えると [Date::Error](https://docs.ruby-lang.org/ja/3.0.0/class/Date=3a=3aError.html) を発生させるのでした：

```rb
require "date"

Date.new(2022, 2, 31)
# => Date::Error
```

そこで，コード 1 では [begin/rescue/end](https://docs.ruby-lang.org/ja/3.0.0/doc/spec=2fcontrol.html#begin) を使ってその例外を捕捉するようにしています。


また，日曜日かどうかの判定に [Date#wday](https://docs.ruby-lang.org/ja/3.0.0/method/Date/i/wday.html) を使っています。
「wday」というメソッド名は「曜日」の英語「day of week」に由来するのでしょう。
`wday` が「日曜日のとき 0 を返す」という知識が必要です。`date.wday == 0` は分かりにくい（≒バグを生みやすい）コードと言えるでしょう。
「日曜始まり」としっかり覚えていればいいですが，これがもし木曜日だったら指折り数えて確認するのではないでしょうか。

## コード 2

コード 2 は上記二点について改善されています。

まず全ての日付を生成するのに，「Date オブジェクトを始端・終端とする範囲オブジェクト」および [Range#each](https://docs.ruby-lang.org/ja/3.0.0/method/Range/i/each.html) メソッドを使いました。

範囲オブジェクトは，`1..7` のように，数値を始端・終端とするものをよく使いますが，数値以外も可能です。
大雑把に言って，`<=>` で比較が可能なオブジェクトを二つ用意すれば，それを始端・終端とする範囲オブジェクトが作れます。
そして `Range#each` が使えるための要件は，始端が `succ` メソッドを持ち，その返り値もまた `succ` メソッドを持ち……となっていて，終端と比較できる，ということです。
`succ` は「次の値（successor）」を与えるメソッドとして用いられます。

[Date#succ](https://docs.ruby-lang.org/ja/2.7.0/method/Date/i/next.html) は「次の日付」を返すメソッドですね。
次のように，（当然ながら）閏年も正しく処理して「次の日付」を与えてくれます：

```rb
require "date"

puts Date.new(2020, 2, 28).succ # => 2020-02-29
puts Date.new(2021, 2, 28).succ # => 2022-03-01
```

だから，2022 年のすべての日付についてのイテレーションが

```rb
require "date"

(Date.new(2022, 1, 1)..Date.new(2022, 12, 31)).each do |date|
  # なんとかかんとか
end
```

と書けるわけです。

次に，日曜日かどうかの判定ですが，そのものズバリの [Date#sunday?](https://docs.ruby-lang.org/ja/2.7.0/method/Date/i/sunday=3f.html) というメソッドが存在しています。もちろん，すべての曜日についてこのようなメソッドが用意されています。
これを使えば分かりやすいコードになりますね。

しかし，もっと簡素に書けます。

# 改善

コード 1 もコード 2 も，「カウンター変数を初期化して，条件によりカウントアップする」ということをあらわに書いているのが**イケてない**ですね。

何かを数えるのに活躍するのが [Enumerable#count](https://docs.ruby-lang.org/ja/2.7.0/method/Enumerable/i/count.html) です。
これを使うと以下のように書けます。

```rb
require "date"

date_range = Date.new(2022, 1, 1)..Date.new(2022, 12, 31)
puts date_range.count{ |date| date.sunday? }
```

Range クラスには `each` メソッドが実装されており，かつ [Enumerable](https://docs.ruby-lang.org/ja/2.7.0/class/Enumerable.html) モジュールが include されています。
`Enumerable` モジュールは，`each` メソッドを持つものに対して，`each` を用いたさまざまな便利なメソッドを提供するためのものです。

`Enumerable#count` は，`each` を使って要素を一つずつ取り出してブロックを評価し，その評価値（いわゆる「ブロックの返り値」）が真であるものの数を数えるメソッドです。

これで，動作が理解できたことになりますが，さらに短く書くこともできます。
もし，

```rb
p [1, 4, 9].map(&:to_s) # => ["1", "4", "9"]
```

のようなコードに馴染みがあるなら，

```rb
require "date"

date_range = Date.new(2022, 1, 1)..Date.new(2022, 12, 31)
puts date_range.count(&:sunday?)
```

と書けることに気づくでしょう。
この書き換えは，単に字数が減ったというよりは簡潔になったのです。好みはあるかもしれませんが，思考の経済の面でこちらのほうが望ましいと私は思います。

ただし，なぜこのような書き方ができるかを理解するためには，少しく Ruby の学習が必要です。
一言で表現するなら，「メソッド呼び出しにおいてブロックを与える代わりにシンボルオブジェクトを `&` 付きで与えると，[Symbol#to_proc](https://docs.ruby-lang.org/ja/2.7.0/method/Symbol/i/to_proc.html) によって [Proc](https://docs.ruby-lang.org/ja/2.7.0/class/Proc.html) オブジェクトが作られ，それがブロックとしてメソッドに与えられる」となります。

# 余録（2021/12/31 追記）

いちいち全日付の Date オブジェクトを生成しないで計算で求めたらどうでしょうか？

まず，2022 年の日数ですが，これは，2023 年の元旦と 2022 年の元旦の差演算（つまり [Date#-](https://docs.ruby-lang.org/ja/3.0.0/method/Date/i/=2d.html)）で得られます：

```rb
require "date"

first_day = Date.new(2022, 1, 1)
days = first_day.next_year - first_day
```

この値（`days`）はどういうわけか Integer オブジェクトではなく，分母が 1 の Rational オブジェクトです。

次に，この日数の中に日曜日がいくつ入っているかを考えます。
もしも一年が日曜始まりだったら話は簡単で，日数を 7 で割って切り上げればいいですね。
つまり

```rb
days.quo(7).ceil
```

となります。

[Numeric#quo](https://docs.ruby-lang.org/ja/3.0.0/method/Numeric/i/quo.html) というメソッドはあまり馴染みがないかもしれませんが，除数・被除数のクラスに関わらず（整商ではない）商を返すメソッドです。
`/` が除数・被除数のクラスによって商だったり整商だったりするのとは違うわけですね。

もっとも，先ほど述べたように `days` は Integer ではなく Rational なので，

```rb
(days / 7).ceil
```

と書いても結果は変わりません。しかし，`quo` を使ったほうが（整商ではなく）商であることが明確になっていいと思います。

次に，一年が日曜始まりとは限らない一般の場合を考えます。
このときは，一年の日数から「最初の日曜が出てくるより前の日数」を差し引いてやればいいだけですね。

差し引く日数は以下のようになります。あとのために，Date オブジェクトの曜日を表すメソッド `wday` の値も併記しました。

| 元旦の曜日 | 差し引く日数 | `wday` |
|:-:|-:|-:|
| 日曜 | 0 | 0 |
| 月曜 | 6 | 1 |
| 火曜 | 5 | 2 |
| 水曜 | 4 | 3 |
| 木曜 | 3 | 4 |
| 金曜 | 2 | 5 |
| 土曜 | 1 | 6 |

となります。
元旦の日付を `first_day` とすると，

```rb
(7 - first_day.wday) % 7
```

を差し引けばよいと分かります。
Ruby の，負数に対する `%` の振る舞いをきちんと理解していれば，これは

```rb
-first_day.wday % 7
```

と書いても構わないことが分かりますが，まあ前者のほうが分かりやすいかと思います（他言語から来た方を惑わせませんし）。

以上をまとめると

```rb
require "date"

first_day = Date.new(2022, 1, 1)
days = first_day.next_year - first_day
puts (days - (7 - first_day.wday) % 7).quo(7).ceil
```

となります。