Ruby練習問題と回答まとめ

ただただRuby練習問題を毎日解いていくまとめです。

練習問題を解いている背景はこちら。 qiita.com

リファクタリングのまとめはこちら。

aoki-shiraki21.hatenablog.com

helloの繰り返し

# Hello World![改行]を5回表示させてください。
# print等は1回の使用にとどめてみてください。
# 可能ならコマンドラインから入力を受け取って、n回表示するように改造してください。

とりあえずの回答。コンソールで受け取った数字に対してHello Worldを繰り返して表示する。普通にやったらここまでで学習が終わってしまう。

num = gets.to_i

num.times do |n|
  print "Hello World! ", n+1, "\n"
end

このままだと、毎回手入力する必要があるため、変更を加える。関数にしておいて、数字を指定できるようにしておく。

-関数化する

def repeat_hello(n)
  n.times do |n|
    print "Hello World! ", n+1, "\n"
  end
end

repeat_hello(5)

ただ、このままだとまさにputs病な回答になってしまう。 そこで以下の修正を加える。

  • ロジック本体とコンソール出力用に分解
  • minitestを追加
require 'minitest/autorun'

def repeat_hello(n)
# ロジック本体
  rows = []
  n.times do |n|
    rows.push("Hello World! #{n + 1}")
  end

  rows
end

def main
  # コンソール出力用
  rows = repeat_hello(5)
  puts rows
end

class RepeatHello < Minitest::Test
  def test_repeat_hello
    assert_equal ['Hello World! 1', 'Hello World! 2', 'Hello World! 3', 'Hello World! 4', 'Hello World! 5'], repeat_hello(5)
  end
end

改めて見ると、テストの文が長いのが気になる。 安全にチェックするために、テストのパターンが複数試せた方が良いので、以下の修正を行う。

  • テストパターンの事前にテストデータ化
require 'minitest/autorun'

def repeat_hello(n)
  # ロジック本体
  rows = []
  n.times do |n|
    rows.push("Hello World! #{n + 1}")
  end

  rows
end

def main
  # コンソール出力用
  rows = repeat_hello(5)
  puts rows
end

class RepeatHello < Minitest::Test

  def test_repeat_hello

# テストデータの作成
    test_data = [
      [],
      ['Hello World! 1'],
      ['Hello World! 1', 'Hello World! 2'],
      ['Hello World! 1', 'Hello World! 2', 'Hello World! 3'],
      ['Hello World! 1', 'Hello World! 2', 'Hello World! 3', 'Hello World! 4'],
      ['Hello World! 1', 'Hello World! 2', 'Hello World! 3', 'Hello World! 4', 'Hello World! 5']
      ]

# テストの繰り返し実施
    test_data.each_with_index do |expected, id|
      assert_equal expected, repeat_hello(id)
    end
  end
end

※mapを使えば空の配列を作る必要がない。

FizzBuzz

* なんか偉い人が考えた問題
* ルールは以下の通り
    * 1から順番に数を表示する
    * その数が3で割り切れるなら"Fizz"、5で割り切れるなら"Buzz"、両方で割り切れるなら"FizzBuzz"と表示する
* 要するに"1 2 Fizz 4 Buzz Fizz 7 8 Fizz Buzz ・・・"と出力される
* プログラマかどうかがわかるんだとさ

とりあえずの回答。早速mapを使ってみた。

require "minitest/autorun"

def fizzbuzz(num)
  (1..num).map { |i|
  if i % 15 == 0
  "FizBuzz"
elsif i % 5 == 0
  "Buzz"
elsif i % 3 == 0
  "Fizz"
else
  i
end
    }
end

class FizzBuzzTest < Minitest::Test
  def test_fizzbuzz
    assert_equal [1,2,"Fizz",4,"Buzz","Fizz",7,8,"Fizz","Buzz"],fizzbuzz(10)
  end
end

色々できそう。

techracho.bpsinc.jp

リファクタリングの参考に。

qiita.com

例えば、こんなのはどうだろうか。if節が一つになっているので、スマート。if~elseには3項演算子が使える。

def fizzbuzz(num)
  (1..num).map { |i|
    ret = ''
    ret = 'Fizz' if i % 3 == 0
    ret += 'Buzz' if i % 5 == 0
    ret.empty? ? i : "#{ret}"
    }
end

mapの中2文にしても書けるな。式展開を使った。

def fizzbuzz(n)
  (1..n).map { |i|
    fizzbuzz = "#{"Fizz" if i%3 == 0}#{"Buzz" if i%5 == 0}"
    fizzbuzz.empty? ? i : fizzbuzz
  }
end

素数判定

* 与えられた数が素数かどうか調べる
* あるいは与えられた数までの素数を列挙する
* 処理にかかった時間を計測しておくと、自分の技術向上に伴って処理時間が短くなっていくのがよくわかる

とりあえずの回答。

require "minitest/autorun"

def prime_num(n)
  if n == 1
    "not prime"
  elsif n == 2
    "prime"
  elsif n == 3
    "prime"
  else
    (2...n).each do |i|
      if n % i == 0
        return "not prime"
        break
      end
      return "prime"
    end
  end
end


class PrimeNum < Minitest::Test
  def test_primenum
    test_data = [
      [1, "not prime"],
      [2, "prime"],
      [3, "prime"],
      [4, "not prime"],
      [5, "prime"],
      [6, "not prime"]
    ]

    test_data.each do |input, value|
      assert_equal value,prime_num(input)
    end
  end
end

反則に近いけど、モジュールを使ってしまうやり方。ただちょっと遅いか。

require "prime"

class PrimeNum < Minitest::Test
  def test_primenum
    test_data = [
      [1, false],
      [2, true],
      [3, true],
      [4, false],
      [5, true],
      [6, false]
    ]

    test_data.each do |input, value|
      assert_equal value,Prime.prime?(input)
    end
  end
end

ちょっとスリムになったか。ifを1文で書く記法と、n-1まで調べなくてもsqrt(n)まで分かればいいよねという。

定義から、どこまで調べればいいのかを把握するのは大事だな。

def prime_num(n)
  return false if n == 1
  return true if n == 2 or n == 3
    (2..Math.sqrt(n).to_i).each do |i|
      if n % i == 0
        return false
        break
      end
      return true
    end
end

もう少し整えてみる。returnはメソッドを抜けるので、breakを明示する必要はなかった。nが2,3の時もeachを抜けて最後のtrueを返すので、先に書いておく必要がない。

def prime_num(n)
  return false if n == 1
  (2..Math.sqrt(n).to_i).each do |i|
    return false if n % i == 0
  end
  true
end

平方根

* 与えられた数の平方根を求める
* 当然ライブラリは使わない

1/2乗にする。Float型にしておく必要がある。

require "minitest/autorun"

def make_sqrt(num)
  num ** (1/2.0)
end

class SqrtTest < Minitest::Test
  def test_sqrt
    10.times do |i|
      assert_equal Math.sqrt(i), make_sqrt(i)
    end
  end
end

組み込みライブラリ

docs.ruby-lang.org

閏年

入力された整数がグレゴリオ暦(いつも使ってるやつ)でうるう年であるか判定せよ

minitestの呼び出しはスラッシュで。

require 'minitest/autorun'

回答。

require 'minitest/autorun'

def uruu(num)
  return "うるう" if num % 4 == 0
  "not うるう"
end

class UruuTest < Minitest::Test
  def test_uuru
    assert_equal "うるう", uruu(2000)
  end
end

三項演算子を使ったスリム化。ほぼ模範回答じゃないか?

def uruu(num)
  num % 4 == 0 ? "うるう" : "not うるう"
end

と思ったけど閏年の定義が違ったわ

また、うるう年は以下のように定義されています。
* 4で割り切れる年であること
* 4で割り切れても100で割り切れる年はうるう年ではない
* 4で割り切れて100で割り切れても400で割り切れればうるう年となる

これで。

def uruu(num)
  num % 4 == 0 ? "うるう" : "not うるう"
  "not うるう" if num % 100 == 0
  "うるう" if num % 400 == 0
end

Rubyでは最終的に残った値を返すことを使った。けどエラーになる。最後に計算し た結果が評価されてnilになってしまうのか。

じゃあこれで。余事象的な発想。

require 'minitest/autorun'

def uruu(num)
  return "not うるう" if num % 4 != 0
  return "not うるう" if num % 100 == 0 && num % 400 != 0
  "うるう"
end

class UruuTest < Minitest::Test
  def test_uuru
    test_data = [
      [1900, "not うるう"],
      [1980, "うるう"],
      [2000, "うるう"],
      [2019, "not うるう"]
    ]
    test_data.each do |num, expected|
      assert_equal expected, uruu(num)
    end
  end
end

もしくはこの表現なら一行で書けるんだな。if ~ trueって書かなくても、式を評価すればtrueかfalseを返してくれるんだな。そりゃそうか。

結果がブール値ではなくても、!!で変換することもできるしな。

qiita.com

def uruu(num)
  num % 400 == 0 || (num % 100 != 0 && num % 4 == 0)
end

class UruuTest < Minitest::Test
  def test_uuru
    test_data = [
      [1900, false],
      [1980, true],
      [2000, true],
      [2019, false]
    ]
    test_data.each do |num, expected|
      assert_equal expected, uruu(num)
    end
  end
end

ベンチマークのライブラリーで速度を測れる。

qiita.com

転置行列

入力された行列の転置行列を求めよ

普通に組み込みのライブラリを使うなら、Array.transpose。これをテストに使う。

docs.ruby-lang.org

こんな感じ。

require 'minitest/autorun'

def tenchi(n)
  (0..(n.length-2)).each do |col|
    (1..(n.length-1)).each do |row|
      n[col][row], n[row][col] = n[row][col], n[col][row]
    end
  end
  n
end

class TenchiTest < Minitest::Test
  def test_tenchi
    assert_equal [[1,2],[3,4]].transpose , tenchi([[1,2],[3,4]])
  end
end

これだと33まではいいが、44になったら論理破綻する?

これでもダメか。lを空の配列にしないといけないのかな。l=nってやっちゃうと、変数が指している共通のオブジェクトを指してしまうからダメみたいなことだっけか。

require 'minitest/autorun'

def tenchi(n)
  l = n
  n.length.times do |row|
    n.length.times do |col|
      l[row][col] = n[col][row]
    end
  end
  l
end

class TenchiTest < Minitest::Test
  def test_tenchi
    assert_equal [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]].transpose , tenchi([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]])
  end
end

これだと行けた。2次元配列の作り方はこちら参照。

www.sejuku.net

require 'minitest/autorun'

def tenchi(n)
  l = Array.new(4).map{Array.new(4)}
  n.length.times do |row|
    n.length.times do |col|
      l[row][col] = n[col][row]
    end
  end
  l
end

class TenchiTest < Minitest::Test
  def test_tenchi
    assert_equal [[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]].transpose , tenchi([[1,2,3,4],[5,6,7,8],[9,10,11,12],[13,14,15,16]])
  end
end

Rubyの変数はオブジェクトにつけるラベルだとして考える

qiita.com

  • 行列それぞれの数を変更できるようにする
  • テストをRSpecにする

ロジック部分

def tenchi(list)
  row = list.size
  col = list[0].size
  new_list = Array.new(col) { |list| list = Array.new(row,0)}
  list.each_with_index do |line, first_index|
    line.each_with_index do |element, second_index|
      new_list[second_index][first_index] = element
    end
  end
  new_list
end

テスト部分

require_relative '../lib/hello'

RSpec.describe "tenchi" do

  let(:array) { Array.new(row){|line| line = Array.new(col){rand(10)}} }

  context "3*3行列の時" do
    let(:row) { 3 }
    let(:col) { 3 }
    it "test_3*3" do
      array1 = array
      expect(tenchi(array1)).to eq array1.transpose
    end
  end

  context "3*4行列の時" do
    let(:row) { 3 }
    let(:col) { 4 }
    it "test_3*4" do
      array2 = array
      expect(tenchi(array2)).to eq array2.transpose
    end
  end
end

数当てゲーム

これは答えの数を探すゲームです。適当な数を入れると正解よりも大きいか小さいか,または正解であるか出力されます。それを繰り返すことで答えを探すことができます。このゲームを作成しなさい。答えの数は乱数を使って毎回別の答えを用意しましょう。

整数を返すだけなら、普通にrandでいいのね

techacademy.jp

これでゲームの要件は満たした。

def high_low(expected, predict)
  return "low" if predict < expected
  return "high" if predict > expected
  "ok"
end

default = rand(1..100)
check = ""
while check != "ok"
  user_predict = gets.to_i
  check = high_low(default, user_predict)
  puts check
end

自動で解かせてみる。毎回1もしくは100との間をとるから、まだ無駄が多いな。

def high_low(expected, predict)
  return "low" if predict < expected
  return "high" if predict > expected
  "ok"
end

default = rand(1..100)
check = ""
user_predict = rand(1..100)
while check != "ok"
  user_predict = rand(user_predict..100) if check == "low"
  user_predict = rand(1..user_predict) if check == "high"
  puts user_predict
  check = high_low(default, user_predict)
  puts check
  sleep(1)
end

どうせなら自動化をと書いてみたがうまくいかず。これまた浅いコピー問題か。

def high_low(expected, predict)
  return "low" if predict < expected
  return "high" if predict > expected
  "ok"
end

default = rand(1..100)
check = ""

lowest_aswer = 1
highest_answer = 100

while check != "ok"
  user_predict = rand(lowest_aswer..highest_answer)
  puts user_predict
  check = high_low(default, user_predict)
  puts check

  if check == "low"
    lowest_answer = user_predict
  elsif check == "high"
    highest_answer = user_predict
  end

  sleep(1)
end

丸々コピーするとややこしくなるが、今回のように一部に代入だと別物として扱ってくれるのかな。

def high_low(expected, predict)
  return "low" if predict < expected
  return "high" if predict > expected
  "ok"
end

default = rand(1..100)
check = ""

answer_range = [1,100]

while check != "ok"
  user_predict = rand(answer_range[0]..answer_range[1])
  puts user_predict
  check = high_low(default, user_predict)
  puts check

  if check == "low"
    answer_range[0] = user_predict
  elsif check == "high"
    answer_range[1] = user_predict
  end

  sleep(1)
end

コンソールで調べてみたけど、やはり、a=bのような形ではなく、配列の一部に代入するような場合、値が引き継がれることはないみたい。

irb(main):001:0> a = [1, 2, 3]
irb(main):002:0> b = 10
irb(main):003:0> a[0] = b
irb(main):004:0> a
=> [10, 2, 3]
irb(main):005:0> b = 20
irb(main):006:0> b
=> 20
irb(main):007:0> a
=> [10, 2, 3]

もっというと、予測する方はrandではなく、常にhighestとlowestの真ん中を取るべきだよね。

これでOK。1000とかにしてもめっちゃ早い。

def high_low(expected, predict)
  return "low" if predict < expected
  return "high" if predict > expected
  "ok"
end

default = rand(1..1000)
check = ""

answer_range = [1,1000]

while check != "ok"
  user_predict = (answer_range[0] + answer_range[1]) / 2
  puts user_predict
  check = high_low(default, user_predict)
  puts check, "\n"

  if check == "low"
    answer_range[0] = user_predict
  elsif check == "high"
    answer_range[1] = user_predict
  end

  sleep(0.5)
end

クラス内のインスタンス変数操作とRSpecを使った標準入力テストの練習

class MatchNumGame
  attr_accessor :num, :expected

  def initialize
    @num = rand(10)
  end

  def get
    @expected = gets.to_i
  end

  def high_low(n)
    if @num < n
      "high"
    elsif @num > n
      "low"
    else
      "ok"
    end
  end

  def play_game
    while true
      this_answer = high_low
      puts this_answer
      break if this_answer == "ok"
    end
  end
end
require './lib/hello'

RSpec.describe "guess_number" do
  describe "first_answer" do
    let(:answer) {MatchNumGame.new}

    before do
      allow(ARGF).to receive(:gets) { 12 }
      answer.get
    end

    it "標準入力の成功" do
      expect(answer.expected).to eq 12
    end

    it "予想数字が大きい" do
      expect(answer.high_low(answer.expected)).to eq "high"
    end
  end
end

配列の2つ目の要素から0にする

配列の先頭はそのままに、先頭以外の要素をすべて0に置き換える。

とりあえずそのままの回答。

def change_to_zero(list)
  (1...(list.length)).each do |i|
    list[i] = 0
  end
  list
end

p change_to_zero([1,2,3,4,5])

fillは配列全てに引数を渡す

docs.ruby-lang.org

fillに範囲を与えてあげれば、一行で書けるな。

require 'minitest/autorun'

def change_to_zero(list)
  list.fill(0, 1...(list.length))
end

class ChangeToZeroTest < Minitest::Test
  def test_change_to_zero
    assert_equal [1,0,0,0], change_to_zero([1,2,3,4])
  end
end

今日はfillを覚えられた。

フィボナッチ数列

フィボナッチ数列の第n項を求めるプログラムを再帰呼出しを用いて書いて下さい。ただしnはコマンドライン引数で得るものとします。

こういう感じか!分かると便利だな。

require 'minitest/autorun'

def fib(n)
  return 1 if n == 1 || n == 2
  fib(n-1) + fib(n-2)
end

class FibTest < Minitest::Test
  def test_fib
    test_data = [
      [1, 1],
      [2, 1],
      [3, 2],
      [4, 3],
      [5, 5],
      [6, 8],
      [7, 13],
      [8, 21]
    ]
    test_data.each do |id, expected|
      assert_equal expected, fib(id)
    end
  end
end
フィボナッチ数列の第n項を求めるプログラムを再帰呼出しを用いずに書いて下さい

こんな感じか。n-1とnをnとn+1に変えれば、何回でも同じ計算ができる

require 'minitest/autorun'

def fib(n)
  a, b = 1, 1
  return 1 if n == 1
  (n-2).times do
    a, b = b, a + b
  end
  b
end

class FibTest < Minitest::Test
  def test_fib
    test_data = [
      [1, 1],
      [2, 1],
      [3, 2],
      [4, 3],
      [5, 5],
      [6, 8],
      [7, 13]
    ]
    test_data.each do |id, expected|
      assert_equal expected, fib(id)
    end
  end
end

ハッシュにブロックを与えると、対応する値がないキーが呼び出されるたびにブロックを評価する https://docs.ruby-lang.org/ja/latest/method/Hash/s/new.html

ハッシュを使うと、再帰関数の結果をメモしながら計算ができる。 https://qiita.com/bloody_snow/items/31e32770514b2b4c28f3

require 'minitest/autorun'

def fib(num)
  fib_hash = Hash.new do |h, n|
    if n < 2
      n
    else
      h[n] = h[n-1] + h[n-2]
    end
  end
  fib_hash[num]
end

class FibTest < Minitest::Test
  def test_fib
    test_data = [
      [1, 1],
      [2, 1],
      [3, 2],
      [4, 3],
      [5, 5],
      [6, 8],
      [7, 13]
    ]
    test_data.each do |id, expected|
      assert_equal expected, fib(id)
    end
  end
end

累乗

Ruby

aのn乗を返すような2引数の関数(メソッド)を下記の方法で作って下さい。ただしa, nは正整数とします。(0や負の数に関しては考慮しなくても結構です。)

できたけどあんまり美しくないなあ。

require 'minitest/autorun'

def expone(a, n)
  b = 1
  n.times do
    b *= a
  end
  b
end

class ExponeTest < Minitest::Test
  def test_expone
    assert_equal 8, expone(2, 3)
  end
end

こういう方法もあるんだな。

require 'minitest/autorun'

def pow(base,exponent)
  raise ArgumentError if exponent < 0
  # 指数が0の時は結果は1です
  return 1 if exponent==0
  # 一旦底を置いておく場所を作る
  bases = []
  # 指数が1なら終了
  while exponent != 1 do
    # 指数が奇数の時、その時の底を一旦横に置いておき、指数を一つ減らす
    if exponent.odd? then
      bases.push( base )
      exponent -= 1
    end
    # 指数を2で割る
    exponent /= 2
    # 底の2乗を新たな底とする
    base *= base
    # 繰り返す
  end
  # 最後に、一旦置いておいたものと現時点での底を全て掛け合わせる
  bases.each{ |past_base| base*= past_base }
  # 結果を返す
  return base
end

class PowTest < Minitest::Test
  def test_pow
    assert_equal 8, pow(2, 3)
  end
end

ベンチマーク測ったら、改善版の方が全然早い。

Benchmark.bm 10 do |r|
  r.report "regular" do
    expone(3, 100000)
  end
  r.report "half" do
    pow(3, 100000)
  end
end
=>
                 user     system      total        real
regular      0.608426   0.164100   0.772526 (  0.790193)
half         0.002767   0.000019   0.002786 (  0.002798)

2次方程式

二次方程式
ax^2+bx+c=0
の解を求める3引数の関数(メソッド)を作って下さい。ただし、aは0ではなく、虚数解は考えなくても結構です。

平方根の書き方。 https://techacademy.jp/magazine/21538

解の公式通りに作った。

require 'minitest/autorun'

def quadratic(a, b, c)
  x = []
  first_answer = -b + Math.sqrt(b**2 - 4*a*c)
  first_answer /= 2*a
  x << first_answer
  second_answer = -b - Math.sqrt(b**2 - 4*a*c)
  second_answer /= 2*a
  x << second_answer
end

class QuadraticTest < Minitest::Test
  def test_quadratic
    assert_equal [-2,-2], quadratic(1, 4, 4)
    assert_equal [3,2],quadratic(1,-5,6)
  end
end

少数の丸め誤差について https://math-fun.net/20190910/2843/

こっちにすると誤差を回避できた。

require 'minitest/autorun'

def quadratic(a, b, c)
  x = []
  first_answer = 2 * c
  first_answer /= -b - Math.sqrt(b**2 - 4*a*c)
  x << first_answer
  second_answer = -b - Math.sqrt(b**2 - 4*a*c)
  second_answer /= 2*a
  x << second_answer
end

class QuadraticTest < Minitest::Test
  def test_quadratic
    assert_equal [-2,-2], quadratic(1, 4, 4)
    assert_equal [3,2],quadratic(1,-5,6)
    assert_equal [-0.001,-1000],quadratic(1,1000.001,1)
  end
end

3の倍数(四則演算なし)

10進整数(e.g. `12345`)が3の倍数かどうかを判定するプログラムを書け。ただし四則演算を使ってはいけない。

回答部分。一旦、2桁までだと仮定した。

def multiple_three(n)
  num_string = n.to_s
  num_size = num_string.size
  if num_size == 2
    if num_string =~ /[147]\d/
      true if num_string =~ /[258]$/
    elsif num_string =~ /[258]\d/
      true if num_string =~ /[147]$/
    else
      true if num_string =~ /[0369]$/
    end
  else
    true if num_string =~ /[369]/
  end
end

正規表現

require './lib/hello'

RSpec.describe "3の倍数かをチェックする" do
  context "3の倍数の場合trueを返す" do
    it { expect(multiple_three(3)).to be true }
    it { expect(multiple_three(6)).to be true }
    it { expect(multiple_three(12)).to be true }
    it { expect(multiple_three(45)).to be true }
  end

  context "3の倍数以外、nilを返す" do
    it { expect(multiple_three(2)).to be_falsey }
    it { expect(multiple_three(13)).to be_falsey }
    it { expect(multiple_three(25)).to be_falsey }
    it { expect(multiple_three(47)).to be_falsey }
  end
end

まあ普通にやったらこれで終わるんですけどね。

def multiple_three(n)
  n % 3 == 0
end

桁が上がっても対応できるように調整。(テストの桁をあげてもパスすることを確認)

def multiple_three(n)
  n_list = n.to_s.chars
  remain = 0
  n_list.each do |each_num|
    each_num_integer = each_num.to_i
    if remain == 0
      remain = 0 if [0,3,6,9].include?(each_num_integer)
      remain = 1 if [1,4,7].include?(each_num_integer)
      remain = 2 if [2,5,8].include?(each_num_integer)
    elsif remain == 1
      remain = 1 if [0,3,6,9].include?(each_num_integer)
      remain = 2 if [1,4,7].include?(each_num_integer)
      remain = 0 if [2,5,8].include?(each_num_integer)
    else
      remain = 2 if [0,3,6,9].include?(each_num_integer)
      remain = 0 if [1,4,7].include?(each_num_integer)
      remain = 1 if [2,5,8].include?(each_num_integer)
    end
  end
  remain == 0
end