ただただRuby練習問題を毎日解いていくまとめです。
練習問題を解いている背景はこちら。 qiita.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
色々できそう。
リファクタリングの参考に。
例えば、こんなのはどうだろうか。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
組み込みライブラリ
閏年
入力された整数がグレゴリオ暦(いつも使ってるやつ)でうるう年であるか判定せよ
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を返してくれるんだな。そりゃそうか。
結果がブール値ではなくても、!!で変換することもできるしな。
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
ベンチマークのライブラリーで速度を測れる。
転置行列
入力された行列の転置行列を求めよ
普通に組み込みのライブラリを使うなら、Array.transpose
。これをテストに使う。
こんな感じ。
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次元配列の作り方はこちら参照。
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の変数はオブジェクトにつけるラベルだとして考える
- 行列それぞれの数を変更できるようにする
- テストを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でいいのね
これでゲームの要件は満たした。
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
は配列全てに引数を渡す
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