Ruby Silverに合格したのでメモなど

先日、「Ruby Silver」こと「Ruby Association Certified Ruby Programmer Silver version 3」を受検し、めでたく合格することができました🙌

www.ruby.or.jp

記録のためにブログを書いていきます📝

受検のきっかけ

Ruby Silverは以前から取りたいと思っていたのですが、なかなか受検することができていませんでした。

昨年11月にフィヨルドブートキャンプを卒業し、年始に「今年こそRuby Silver取らなきゃな〜」と呑気に思っていた矢先、Rubyの基礎力不足を感じる出来事がありました。

やりたいことは多々ありましたがまずこれはRuby Silverを取る機運だろうと思い、勢いで2月7日(土)の受検予約をしたのが1月21日(水)のことでした。

やったこと

予約をしてからは以下の試験対策を中心に行いつつ、気になる部分は公式ドキュメントを確認したり生成AIに類似問題を出してもらうなどして復習していきました。

どれも基本的には「50問やる→採点する→間違えたところを中心にチェック→同じ50問をやり直す」という流れで、1セットで2周解いていました。

あと、特に間違えた問題は必ずirbで実際にコードを実行するようにしました。「ここをこう書き換えたらどうなるかな?」みたいな試行錯誤は楽しかったし、学びも大きかったなと思います。

  • 書籍『最短突破 Ruby技術者認定試験(Silver/Gold対応) 公式テキスト
    • テキスト部分はだいぶ前にひと通り読了済みで、適宜見直していました
    • 基礎力確認問題(30問)と模擬試験(50問)を4セットほどやりました
    • 連続でやり過ぎると回答自体を覚えてしまうので、最後は1週間ほど空けて、前日に最後の1回を解くように逆算して実施しました
  • GitHubに上がっている公式の模擬試験『Ruby技術者認定試験Silver模擬問題
    • 2セットほどやりました
  • Ruby技術者認定試験の対策サイト『REx
    • 7セットほどやりました
    • 一番回数が多いのは、一番手軽にできたからです

受検当日

試験会場は池袋のテストセンターでした。家族も一緒に出かけて、サンシャイン水族館に行ってもらっている間に受検してきました。(僕もサンシャイン水族館行きたかった😇)

試験時間は90分。時間は充分に余裕がありましたが、自信のない問題が10問ほどありました。何度も見直しをして、「よほどの勘違いがない限り自信のないやつが全部不正解でも80点は取れてるはず」という状態で試験を終了、結果は84点でした。
できれば90点以上取りたかったところですが、引き続き精進していきたいと思います。

「合格」の部分、ちょい要素が下にズレているのを修正したい

受検を終えて

試験勉強として繰り返し問題を解いたことで、Rubyの基本的な内容について復習できて良かったです。

時は大AI時代、資格試験の勉強がどこまで役に立つのか?という考え方もあるかもしれませんが、基本的なところから毎回AIに聞いていてはキリが無いですし、AIを使いこなすためにも自分自身の知識をちゃんと定着させていきたいと思っています。なので個人的には受検して良かったですし、勉強の楽しさや達成感を改めて感じることができたなーとも思います。

おわりに

無事合格できて、ホッと胸を撫で下ろしました。
次は基本情報技術者試験を取ろうと思います🚀

www.ipa.go.jp

おまけ1:公式模擬試験にPRを出した

GitHubの模試をやっている途中に問題文の並びに違和感がある箇所があったので、修正のPRを出しました。マージされるといいな✨

github.com

おまけ2(長文):Ruby Silver対策メモ

今回、勉強中に何度も間違えてしまったところやうっかりミスしそうになったことを適宜メモしていました。
せっかくなのでおまけとして載せておきます!

(もしおかしいところがありましたら、そっとご指摘ください🙏)

その他

「その他」から紹介するのも変ですが、カテゴライズしにくかったやつ

  • Stringクラスにto_hメソッドは無い(Arrayクラスにはある)
"hoge".to_h # => エラー
[[:price, 100], [:amount, 5]].to_h # => {price: 100, amount: 5}
  • Stringクラスにbinaryメソッドは無い
"7".binary # => エラー
  • 式展開の省略記法(以下のようにそれぞれ{}を省略可能)
    • "#{$hoge}""#$hoge"
    • "#{@hoge}""#@hoge"
  • メソッド内では定数を定義できない
def hoge
  x = 10
  Y = x < 10 ? "C" : "D"
  puts Y
end
hoge
# => SyntaxErrorが出る

strip, chomp, chop

chomp はAtCoderでよく使っていて無意識に覚えていたので助かりました。

  • strip:文字列の先頭と末尾の空白文字(\t\r\n\f\v)を取り除く
  • chomp:末尾から改行コード("\r\n", "\r", "\n")を取り除く
  • chop:末尾の文字を取り除く。ただし、文字列の末尾が"\r\n"であれば2文字とも取り除く
  • 上記それぞれ、strip! chomp! chop! の破壊的メソッドもある
s = "hello\n\n"

s.chomp  # => "hello\n"
s.chomp  # => "hello\n" # 非破壊的メソッドのため1行上の結果は無かったことになる
s.length # => 7

s.chomp! # => "hello\n"
s.chomp! # => "hello" # 破壊的メソッドのため1行上の結果が保持される
s.length # => 5

pop, push, unshift, shift

⚠️! が無いけど、全部破壊的メソッド

  • popは、末尾の1要素を破壊的に取り出す。引数nがある場合は末尾からn個の要素を取り出して配列で返す
  • pushは、末尾に引数の値を破壊的に追加する
  • shiftは、先頭の1要素を破壊的に取り出す。引数nがある場合は先頭からn個の要素を取り出して配列で返す
  • unshiftは、先頭に引数の値を破壊的に追加する
a = [1, 2, 3, 4, 5] # => [1, 2, 3, 4, 5]
a.pop # => 5
a # => [1, 2, 3, 4]
a.pop(2) # => [3, 4]
a # => [1, 2]
a.push(9) # => [1, 2, 9]
a # => [1, 2, 9]

b = [1, 2, 3, 4, 5] # => [1, 2, 3, 4, 5]
b.shift # => 1
b # => [2, 3, 4, 5]
b.shift(2) # => [2, 3]
b # => [4, 5]
b.unshift(9) # => [9, 4, 5]
b # => [9, 4, 5]

concat, append

  • self.concat(other)self(文字列)にotherを繋げる
  • self.append(other)self(配列)の末尾にotherを追加する
a = "hello"
a.concat(" world") # => "hello world"
a # => "hello world"

b = "hello"
b.concat(" world", "!") # => "hello world!" # 引数は複数渡せる
b # => "hello world!"

c = [1, 2]
c.append(3) # => [1, 2, 3]
c # => [1, 2, 3]

d = [1, 2]
d.append(3, 4) # => [1, 2, 3, 4] # 引数は複数渡せる
d # => [1, 2, 3, 4]

find, detect, select, filter, find_all

  • find, detect:ブロックの値が真になる最初の要素を返す
  • select, filter, find_all:ブロックの値が真になる全ての要素を返す
a = [1, 2, 3, 4, 5, 6]

# 最初の1つだけ返す
a.find { |x| x > 3 }       # => 4
a.detect { |x| x > 3 }     # => 4(findのエイリアス)

# 条件に合う全部を返す
a.select { |x| x > 3 }     # => [4, 5, 6]
a.filter { |x| x > 3 }     # => [4, 5, 6](selectのエイリアス)
a.find_all { |x| x > 3 }   # => [4, 5, 6](selectのエイリアス)

ファイルオープンのモード(r, r+,w, w+, a, a+

これ、覚えるのに苦労しました😇

  • r(readの略):読み込み専用(ファイルが無い場合はエラー)
  • r+:読み書き両方(ファイルが無い場合はエラー)
  • w(writeの略):書き込み専用(新規作成 or 開いた時に中身を消して先頭から書き込む)
  • w+:読み書き両方(新規作成 or 開いた時に中身を消して先頭から書き込む)
  • a(appendの略):追記専用(中身を残して末尾に追記される or 新規作成)
  • a+:読み書き両方(中身を残して末尾に追記される or 新規作成)
  • +は全て「読み書き両方」になる
    • r+は、読む+書く
    • w+は、書く+読む
    • a+は、書く(追記)+読む
    • 特にr+w+は同じ意味に見えるが、 r+は「読む」が起点なので、ファイルがない場合にエラー
  • オープンモードを省略したら、デフォルトのrになる
  • rewindは、「先頭に戻る」(ただし a, a+ では読み込み位置は戻るが、書き込みは常に末尾)
# `test.txt` の中身は `"ABC"`とする
File.open("test.txt", "a+") do |f|
  f.rewind # 読み込み位置が先頭へ
  f.write("Z") # a+モードは、書き込みは常に末尾
  f.rewind # 読み込み位置が先頭へ
  puts f.read
end
# => "ABCZ"

product, zip, push, transpose, combination, permutation

combinationpermutation は問題ではほぼ見かけなかったけど、セットで覚えました。

  • product:全組み合わせ(直積)を作る
  • zip:同じインデックス同士をペアにする(要素数はレシーバに合わせる)
  • push:配列の末尾に引数をそのまま1要素として追加する(展開されない)
  • transpose:配列の行と列を入れ替える(要素数が揃っていないとIndexError)
  • combination:順序を区別しない組み合わせ
  • permutation:順序を区別する順列
# productとzipの基本
[1, 2].product([3, 4]) # [[1, 3], [1, 4], [2, 3], [2, 4]]
[1, 2].zip([3, 4]) # [[1, 3], [2, 4]]

# レシーバと引数の要素数が異なる場合
[1, 2].product([3, 4, 5]) # => [[1, 3], [1, 4], [1, 5], [2, 3], [2, 4], [2, 5]]
[1, 2, 3].product([4, 5]) # => [[1, 4], [1, 5], [2, 4], [2, 5], [3, 4], [3, 5]]
[1, 2, 3].zip([4, 5]) # => [[1, 4], [2, 5], [3, nil]]
[1, 2].zip([3, 4, 5]) # => [[1, 3], [2, 4]]

[[1, 2]].push([3, 4]) # => [[1, 2], [3, 4]]
[[1, 2]].push([3, 4, 5]) # => [[1, 2], [3, 4, 5]]
[[1, 2, 3]].push([4, 5]) # => [[1, 2, 3], [4, 5]]

[[1, 2], [3, 4]].transpose # => [[1, 3], [2, 4]]
[1, 2].product([3, 4]).transpose # => [[1, 1, 2, 2], [3, 4, 3, 4]]
[[1, 2], [3, 4, 5]].transpose # element size differs (3 should be 2) (IndexError)

[1, 2, 3].combination(2).to_a  # => [[1, 2], [1, 3], [2, 3]]
[1, 2, 3].permutation(2).to_a  # => [[1, 2], [1, 3], [2, 1], [2, 3], [3, 1], [3, 2]]

sub, gsub, slice, scan

⚠️ sub! gsub! slice! の破壊的メソッドがあるが、scanだけは無い

  • sub:マッチした最初の箇所のみ置換
  • gsub:マッチした全ての箇所を置換
  • slice:最初にマッチした文字列を返す
  • scan:マッチした文字列を配列で返す
"hello world".sub(/o/, "0")  # => "hell0 world"
"hello world".gsub(/o/, "0") # => "hell0 w0rld"
"hello world".slice(/o/)     # => "o"
"hello world".scan(/o/)      # => ["o", "o"]

IOクラスのクラスメソッド(抜粋)

⚠️ IOFileの親クラス(superclass)なので、IOにあるクラスメソッドはFileでも使える

  • IO.open # ファイルを開く
  • IO.read # ファイルを読む
  • IO.write # ファイルに書く
  • IO.readlines # 全行を配列で読む
  • IO.foreach # 1行ずつ処理
  • IO.select # IO監視

Fileクラスのクラスメソッド(抜粋)

クラスメソッド系はなかなか覚えられなかった…

  • ファイル操作

    • File.open # 開く
    • File.read # 読む
    • File.write # 書く
    • File.delete # 削除 ⚠️Dirクラスにもある
    • File.rename # 名前変更(⚠️Dirクラスメソッドじゃないけどディレクトリにも使える)
    • File.chmod # パーミッション変更
    • File.chown # オーナー変更
    • File.exist? # 存在するか ⚠️Dirクラスにもある
    • File.size # サイズ
    • File.mtime # 更新日時
  • パス文字列の操作

    • File.basename # パスからファイル名を取り出す
    • File.dirname # パスからディレクトリ名を取り出す ⚠️Dirクラスっぽいけど違う
    • File.extname # パスから拡張子を取り出す
    • File.join # パスを結合
    • File.split # パスを分割

Dirクラスのクラスメソッド(抜粋)

覚え方:Linuxコマンドっぽい名前(pwd, cd, mkdir, rmdir)
Linuxコマンドを覚えていないのでこの覚え方はあまり活用できなかった😇)

  • Dir.pwd # 今いるディレクトリ
  • Dir.chdir # ディレクトリを移動
  • Dir.mkdir # ディレクトリを作る
  • Dir.delete # ディレクトリを削除 ⚠️Fileクラスにもある
  • Dir.rmdir # 同上(エイリアス)
  • Dir.unlink # 同上(エイリアス)
  • Dir.entries # 中身の一覧を取得
  • Dir.glob # パターンで検索
  • Dir.exist? # 存在するか ⚠️Fileクラスにもある
  • Dir.home # ホームディレクトリ

%記法

  • %!...!: ダブルクォート文字列(式展開:あり、%Qの省略形)
  • %Q(...): ダブルクォート文字列(式展開:あり、由来:Quote)
  • %q(...): シングルクォート文字列(式展開:なし、由来:quote)
  • %W(...): 文字列の配列(式展開:あり、由来:Words)
  • %w(...): 文字列の配列(式展開:なし、由来:words)
  • %I(...): シンボルの配列(式展開:あり、由来:Identifiers)
  • %i(...): シンボルの配列(式展開:なし、由来:identifiers)
  • %s(...): シンボル1つ(式展開:なし、由来:Symbol)
  • %r(...): 正規表現(式展開:あり、由来:Regexp
  • %x(...): コマンド実行(式展開:あり、由来:eXecute)
lang = "Ruby"
%w(#{lang} Python Java) # => ["\#{lang}", "Python", "Java"] # 式展開されない
%W(#{lang} Python Java) # => ["Ruby", "Python", "Java"]     # 式展開される

%i(foo bar baz).class # => Array
%i(foo bar baz).first.class # => Symbol

正規表現

語源から覚えるの大切だなと思いました。

  • 小文字は、マッチするもの
    • \d(digitの略):数字(0-9)にマッチ
    • \w(wordの略):単語構成文字(a-z, A-Z, 0-9, _)にマッチ
    • \s(spaceの略):空白(スペース、タブ、改行)にマッチ
  • 大文字にすると「否定」
    • \D:数字以外
    • \W:単語構成文字以外
    • \S:空白以外
  • 位置を示すもの
    • ^:行の先頭(複数行で各行にマッチ)
    • $:行の末尾(複数行で各行にマッチ)
    • \A:文字列の先頭(文字列全体の最初だけ)
    • \z:文字列の末尾(文字列全体の最後だけ)
  • 量を示すもの
    • *:0回以上(a* → "", "a", "aaa")
    • +:1回以上(a+ → "a", "aaa"(""は×))
    • ?:0回か1回(a? → "", "a")
    • {n}:ちょうどn回(a{3} → "aaa")
    • {n,m}:n回以上m回以下(a{2,4} → "aa", "aaa", "aaaa")
  • その他
    • .:任意の1文字(改行以外)
    • []:文字クラス(どれか1つ)
    • [^]:否定の文字クラス
    • ():グループ化
    • =~:マッチした位置(インデックス)を返す。マッチしなければnilを返す
s = "abc\ndef\nghi"
s.scan(/^\w+/) # => ["abc", "def", "ghi"]
s.scan(/\A\w+/) # => ["abc"]

"Ruby123" =~ /\d+/ # => 4
"Ruby123"[/\d+/] # => "123"

"hello" =~ /x/ # => nil
"hello" =~ /l/ # => 2
"hello".match?(/x/) # => false # match? はtrue/falseを返す

進数

  • 2進数 0b、使える数字:0, 1
    • 0b0001(1)、0b0110(6)、0b1010(10)
  • 8進数 0o または 0、使える数字:0-7
    • 0o10(8)、010(8)、0o25(21)、025(21)
  • 10進数 プレフィックスなし、使える数字:0-9
    • 10(10)、010025は8進数になるので注意
  • 16進数 0x、使える数字:0-9, a-f(A-Fも可)
    • 0x10(16)、0x1F(31)、0x90(144)
  • 関連メソッド
  • to_i(基数) 文字列を指定した基数で整数に変換
    • "10".to_i(2)(2)、"FF".to_i(16)(255)
  • to_i(0) プレフィックスから基数を自動判定
    • "0x10".to_i(0)(16)、"0b10".to_i(0)(2)
  • .hex 16進数文字列を整数に変換
    • "0x90".hex(144)、"FF".hex(255)
  • .oct 8進数文字列を整数に変換
    • "10".oct(8)、"77".oct(63)
0x10 # => 16 # 16進数の10は16
010  # => 8  # 8進数の10は8
0b10 # => 2  # 2進数の10は2

"0xFF".to_i     # => 0   # 引数無しなので10進数として解釈、xの時点で変換が止まり0になる
"0xFF".to_i(16) # => 255 # 16進数として解釈。`0x` プレフィックスも認識 → 255
"0xFF".to_i(0)  # => 255 # プレフィックスから自動判定。`0x` → 16進数 → 255
"0xFF".hex      # => 255 # 16進数文字列として変換 → 255

ヒアドキュメントの開始ラベル

  • "識別子"(ダブルクオート):式展開が有効
  • 識別子(クオート無し):ダブルクオートと同じ
  • '識別子'(シングルクオート):式展開できない
  • `識別子`(バッククオート):コマンド出力
name = "Ruby"

# 基本形:終端ラベルは行頭に置く必要がある
a = <<EOS
Hello #{name}
EOS

# <<- :終端ラベルをインデントできる
b = <<-EOS
  Hello #{name}
  EOS

# <<~ :終端ラベルをインデント可能 + 中身の共通インデントを自動除去
c = <<~EOS
  Hello #{name}
  EOS

# シングルクォート:式展開なし
d = <<'EOS'
Hello #{name}
EOS

# <<- と ' の組み合わせも可能
e = <<-'EOS'
  Hello #{name}
  EOS

p a  # => "Hello Ruby\n"
p b  # => "  Hello Ruby\n"(インデントがそのまま残る)
p c  # => "Hello Ruby\n"(インデントが自動除去される)
p d  # => "Hello #{name}\n"(式展開されない)
p e  # => "  Hello #{name}\n"(式展開されない)