Ruby初心者向けプログラミング問題10選を解いてみた(上)

何か投稿してみようということで、少し前に解いた練習問題を貼っていきたいと思います
高度なテクニックは全く使っていないので、初心者の方も安心してご覧ください

コード、本文ともに見づらいと思いますがご勘弁を…

# 素晴らしき元サイト様
[アウトプットのネタに困ったらこれ！？Ruby初心者向けのプログラミング問題を集めてみた（全10問）](https://blog.jnito.com/entry/2019/05/03/121235)


# カレンダー作成問題
## 問題文
<details><summary>問題文(折りたたみ)</summary><div>
>Date クラスを使って、今月の１日と月末の日付と曜日を求め、次のような形式でカレンダーを表示させてください
>
          April 2013
     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 3
</div></details>

## コード
```Ruby
require "date"

today = Date.today
first_date = Date.parse(today.strftime("%Y.%m.1"))
last_date = (first_date >> 1) - 1

puts today.strftime("%B %Y").center(21)
puts " Su Mo Tu We Th Fr Sa"
print "   " * first_day.wday

(1..last_day.day).each do |day|
  print day.to_s.rjust(3)
  print "\n" if (first_date.wday + day) % 7 == 0 || day == last_day.day
end
```

## 要点解説
```Ruby
today = Date.today
first_date = Date.parse(today.strftime("%Y.%m.1"))
last_date = (first_date >> 1) - 1
```
今日、月初、月末のDateオブジェクトを定義します

月初は、`today`をフォーマットし、その月の一日の文字列を作って`parse`しています
月末は来月の`first_date`の1日前として今月の最終日を求めています

```Ruby
print "   " * first_day.wday
```
`wday`で得られる値は、0から始まる曜日を表す数字です　(日曜→0, 月曜→1・・・という感じ)
その回数空白を出力すれば、1日までの空白を再現できます

```Ruby
  print "\n" if (first_date.wday + day) % 7 == 0 || day == last_day.day
```
1日の位置+現在の日付が7の倍数になったら改行します

# カラオケマシン問題
## 問題文
<details><summary>問題文(折りたたみ)</summary><div>
>かえるのうたのメロディは以下のような文字列で表現されます。
>
>"C D E F |E D C   |E F G A |G F E   |C   C   |C   C   |CCDDEEFF|E D C   "
>
>縦棒( | )は小節の区切りです。
>半角スペースは休符です。
>音符と休符が8つ集まると1小節になります。
>
>このメロディ(文字列)を変更するKaraokeMachineというクラスを作ります。
>このクラスはインスタンス作成時にメロディを受け取ります。
>transposeというメソッドに変化させたいキーの量を渡すと、その分だけキーを上げたり下げたりしたメロディが返却されます。
</div></details>

## コード
```Ruby
class KaraokeMachine
  SCALE = %w(C C# D D# E F F# G G# A A# B).freeze

  def initialize(melody)
    @melody = melody
  end

  def transpose(amount)
    @melody.gsub(/[A-G]#?/) { |sub| SCALE[(SCALE.index(sub) + amount) % 12] }
  end
end
```

## 要点解説
```Ruby
 @melody.gsub(/[A-G]#?/) { |sub| SCALE[(SCALE.index(sub) + amount) % 12] }
```
正規表現での置換を使います
まず｢A-Gのいずれかと#が0か1個ある｣文字列をブロックに渡します
ブロック内部では`SCALE`の`(SCALE.index(sub) + amount) % 12`の音階を返します

# ビンゴカード作成問題
## 問題文
<details><summary>問題文(折りたたみ)</summary><div>
>1から75までの数字をランダムに配置して、ビンゴカードを作成するプログラミング問題です。
ただし、実際のビンゴカードのルールに沿って、各列は以下のような仕様で数字を出力する必要があります。
>
>- B：1～15のどれか
>- I：16～30のどれか
>- N：31～45のどれか
>- G：46～60のどれか
>- O：61～75のどれか

>出力例は以下のとおりです。
>
     B |  I |  N |  G |  O
    13 | 22 | 32 | 48 | 61
     3 | 23 | 43 | 53 | 63
     4 | 19 |    | 60 | 65
    12 | 16 | 44 | 50 | 75
     2 | 28 | 33 | 56 | 68

***
>ビンゴカードを文字列として出力する、Bingo.generate_card メソッドを作成してください。
>（中略）
>Bingo.generate_card メソッドの出力結果は上で述べた数字のルールのほかに、以下の仕様を満たす必要があります。
>
>毎回異なるカードを生成します。
>各列はパイプ（|）で区切ります。
>数字や"BINGO"の文字は右寄せで出力します。
>真ん中（FREEになる場所）はスペースを出力します。
</div></details>

## コード
*ウンコードです

```Ruby
class Bingo
  def self.generate_card
    %w(B I N G O).each_with_index do |chr, i|
      column = ["#{chr}"]
      iex = (i + 1) * 15

      5.times do |c|
        column << rand((iex + 1 - 15)..iex).to_s
        redo if column.uniq!

        if chr == "N" && c == 2
          column.pop
          column << " "
        end
      end

      instance_variable_set("@#{chr}", column)
    end

    @B.zip(@I, @N, @G, @O) do |row|
      puts row.map { |x| x.rjust(2) }.join(" | ")
    end
  end
end
```

## 要点解説
```Ruby
      5.times do |c|
        column << rand((iex + 1 - 15)..iex).to_s
        redo if column.uniq!

        if chr == "N" && c == 2
          column.pop
          column << " "
        end
      end
```
B,I,N,G,Oの文字列の入ったそれぞれの配列にランダムな数字を入れていきます

`(iex + 1 - 15)..iex`はごちゃごちゃしてますが、一応期待通りの範囲オブジェクトを作れます

`uniq!`は重複する値が無いと`nil`を返します
なので、if文の条件式にすると`uniq!`が適用され`column`から重複した値が除かれた場合に`redo`します

```Ruby
     instance_variable_set("@#{chr}", column)
```
第1引数を変数名、第2引数を中身としてインスタンス変数を定義できるメソッドです
苦肉の策です…

# ボーナスドリンク問題
## 問題文
<details><summary>問題文(折りたたみ)</summary><div>
>
ある駄菓子屋で飲み物を買うと、空き瓶3本で新しい飲み物を1本プレゼントしてくれる。
>
>最初に購入した飲み物の本数から、トータルで飲める本数を算出するプログラムを作成せよ。
>
>また、最初に100本購入した場合、トータルで何本飲めるか。

*新しい飲み物が3本揃えばまた一本貰えるというひっかけ問題(?)
</div></details>

## コード
```Ruby
def bounus_drink(amount)
  all_ju = [amount]
  dividend = amount
  loop do
    result = dividend.divmod(3)
    all_ju << result[0]
    dividend = result.sum

    break if dividend < 3
  end
  all_ju.sum
end
```

## 要点解説
```Ruby
  loop do
    result = dividend.divmod(3)
    all_ju << result[0]
    dividend = result.sum

    break if dividend < 3
  end
```
交換できた本数(`dividend / 3`の商)と、交換で手に入れた新しい飲み物も含めた交換できてない本数(`dividend /3`の余り)の合計が3以下になるまで3で割り続けます
再帰処理？ナニソレオイシイノ？

# 電話帳作成問題
## 問題文
<details><summary>問題文(折りたたみ)</summary><div>
>カタカナ文字列の配列を渡すと、ア段の音別にグループ分けした配列を返すプログラムを作成する問題です。
>    
    # INPUT
    ['キシモト', 'イトウ', 'ババ', 'カネダ', 'ワダ', 'ハマダ']
    # OUTPUT 
    [ ['ア', ['イトウ']], ['カ', ['カネダ', 'キシモト']], ['ハ', ['ハマダ', 'ババ']], ['ワ', ['ワダ']] ]
</div></details>

## コード
```Ruby
class NameIndex
  KATAKANAS =[
    ('ア'..'オ').to_a << 'ヴ',
    ('カ'..'ゴ').to_a,
    ('サ'..'ゾ').to_a,
    ('タ'..'ド').to_a,
    ('ナ'..'ノ').to_a,
    ('ハ'..'ボ').to_a,
    ('マ'..'モ').to_a,
    ('ヤ'..'ヨ').to_a,
    ('ラ'..'ロ').to_a,
    ('ワ'..'ン').to_a
  ].freeze

  def self.create_index(names)
    name_index = []
    KATAKANAS.each do |katakana|
      matched = names.select { |name| katakana.include?(name.chr) }
      name_index << [katakana[0], matched.sort] unless matched == []
    end
    name_index
  end
end
```

## 要点解説
```Ruby
      matched = names.select { |name| katakana.include?(name.chr) }
```
`names`から1文字目が`katakana`に含まれいるものを抜き出します
問題文の例でいうと、`katakana`が`('ア'..'オ').to_a << 'ヴ'`だった場合、`matched`は`["イトウ"]`になります

# つづく
以上、前半の5問でした
[Ruby初心者向けプログラミング問題10選を解いてみた(下)](https://qiita.com/porryanes/items/33b705b3fa0488689067)に続く