LoginSignup
6

posted at

updated at

Ruby Gold 3.1の対策と出題傾向について(2022年11月)

最初に

Ruby技術者認定試験がversion 2.1 -> 3.1に改訂されました。
Gold 2.1は受けたことがないので3.1で初めてのGold受験でしたが無事合格することができました。
2023年1月20日までに受ければ落ちても2回目無料キャンペーン中だったのでメンタル的にもちょうど良かったです!

私の場合は昨年シルバー2.1を取得済みだったので、今回ゴールド3.1を取得したことで認定ver3プログラマGoldを名乗れるようです。

新しいバージョンってどうなの??とまだ様子見している方もいらっしゃると思います。
私も試験前にGold 3.1の情報が少なく対策に不安があったので、少しでも参考になればと思い記憶が新しいうちに情報を残そうと思います!

経歴

私の経歴としてはこんな感じです。レベル感の目安になれば。

2021年2月 初めてプログラミング学習開始(ずっと文系でメーカーの営業・事務職でした)
2021年7月 IT企業に入社

~ Ruby on RailsやVue.jsを実務で使う ~

2021年12月 Ruby Silver 2.1合格(92点)

~ Ruby on Railsを実務で使う ~

2022年11月 Ruby Gold 3.1合格(80点)

勉強方法

これまでのバージョンとの違いは公式に出されているこちらの模擬問題集が役に立ちました。
(むしろ最新版試験の対策はこれしかないような・・・)

あとは従来のGold対策と同じようにRExと公式合格教本の問題をひたすら解いて、
解説を読んで理解しておきました。

答え合わせしていて腑に落ちないところなどは必ず自分でコードを書いて動かして確かめることで理解が深まったと思います!

合格教本だけだと理解しにくい箇所もあるので、知識を補うために「プロを目指す人のためのRuby入門」(チェリー本)を大変参考にしておりました。改訂前のものしか持ってなかったのですがProcやメソッド探索、クラス変数などとてもわかりやすく解説していただいているので大変お世話になりました。

2.1と3.1で変わっているところ

旧試験と正誤が変わってくる重要度が高いのがこれらです。
他にも思い出したらまた追加します。

項目 2.1 3.1
privateメソッド レシーバ付きでは呼び出せない レシーバがselfなら呼び出せる
open-uriライブラリ openを再定義する openを再定義しない
(URI.openを使う)
Integerクラス FixnumとBignumに分かれている Integerに統合されている

privateメソッドの挙動コード例

class Sample
  def hoge
    self.fuga # 2.1の問題ではここがエラーになるが3.1では呼び出せる
  end

  private

  def fuga
    puts "fuga"
  end
end
Sample.new.hoge # => fuga

3.1の新しい概念

番号指定パラメータ

_1, _2などの記法。
Numbered Parameters(ナンパラ)。ブロックの仮引数として使用する。
_0ではなく_1から始まる点に注意。

> ["foo", "bar", "baz"].map { _1.upcase }
=> ["FOO", "BAR", "BAZ"]

転送引数

...
引数を転送する記法。
すでに指定されている引数がある場合は残りの全ての引数を転送する。
括弧が必須である点に注意。

def bar(test)
  p test
end

def foo(...)
  bar(...)
end

foo(["Ruby Silver!", "Ruby Gold!"]) # => ["Ruby Silver!", "Ruby Gold!"]
foo("Ruby Gold!") # => "Ruby Gold!"

tally

(公式模擬問題にもなかったし今後出題されるかは不明ですが使いやすそうなので・・)
要素ごとの個数を返すメソッド。
selfに含まれる要素を数え上げた結果をHashで返す。
Hash のキーはselfに含まれる要素で、valueは対応する要素が出現する回数。
結果を加算したい場合には引数にHashオブジェクトを指定する。

idols = ['TWICE', 'IZ*ONE', 'aespa', 'BLACKPINK', 'aespa', 'TWICE']
idols.tally
=> {"TWICE"=>2, "IZ*ONE"=>1, "aespa"=>2, "BLACKPINK"=>1}

["kep1er", "LE SSERAFIM", "IZ*ONE"].tally(idols.tally)
=>
{"TWICE"=>2,
 "IZ*ONE"=>2,
 "aespa"=>2,
 "BLACKPINK"=>1,
 "kep1er"=>1,
 "LE SSERAFIM"=>1}

filter_map

各要素に対してブロックを評価した値のうち、真であった値のみの配列を返すメソッド。
nilだけを除外して返すcompactメソッドと少し異なり、偽の値(つまりfalsenil)を除外する。

(1..10).filter_map { |i| i * 2 if i.even? } #=> [4, 8, 12, 16, 20]
[1, 2, 3].filter_map {|x| x.odd? ? x.to_s : nil } #=> ["1", "3"]
ary = ["foo", "bar", nil, "baz"]
p ary.filter_map { |i| i&.upcase }

# 真の値に対してupcaseされた配列が返る
=> ["FOO", "BAR", "BAZ"]

ary = [1, nil, false, "test"]
p ary.filter_map(&:itself)

# nilとfalseを除外
=> [1, "test"]

flat_map(エイリアス: collect_concat)

各要素をブロックに渡し、その返り値を連結した配列を返すメソッド。
必ず1次元配列を返す訳ではなく、配列を1次元減らすメソッドである点に注意。

# flat_mapメソッドの場合
[[0, 1], [2, 3]].flat_map {|e| e + [100] } # => [0, 1, 100, 2, 3, 100]

# mapメソッドの場合
[[0, 1], [2, 3]].map {|e| e + [100] } # => [[0, 1, 100], [2, 3, 100]]

# 配列を1次元だけ減らす仕様
values = [[[1,2,3]]]
values.flat_map { |value| value } # => [[1, 2, 3]]

# 必ず1次元配列にしたい場合はflattenを使う必要がある
values = [[[1,2,3]]]
values.flat_map { |value| value }.flatten # => [1, 2, 3]

ぼっち演算子

正式名称はsafe navigation operatorと言うらしいです。
レシーバがnilでない時のみ後続のメソッドを呼び出す演算子。

# レシーバがnilだとnilが返る
no_number = nil
no_number&.succ # => nil

# レシーバが有効であれば後続のメソッドが実行される
number = 0
number&.succ # => 1

strptime

DateTime, Date, Timeクラスの特異メソッド。
与えられた雛型で日時表現を解析し、その情報に基づいてオブジェクトを生成する。

  • Time

基本形:
Time.strptime(date, format, now=self.now)
Time.strptime(date, format, now=self.now) {|y| ... }

Time.strptime("00000024021993", "%S%M%H%d%m%Y")
=> 1993-02-24 00:00:00 +0900

# ブロックを渡すと年の部分を変更できる
Time.strptime('91/5/18 4:13:00', '%Y/%m/%d %T'){|y| # ←yにはyear(91)が渡されている
  if y > 100 then y
  elsif y >= 69 then y + 1900
  else y + 2000
  end
}
=> 1991-05-18 04:13:00 +0900

DateクラスとDateTimeクラスにもあり、引数がTimeクラスと異なる

  • DateTime

基本形:
DateTime.strptime(str = '-4712-01-01T00:00:00+00:00', format = '%FT%T%z', start = Date::ITALY)

datetime = DateTime.strptime('2022-01-01T00:00:00+00:00')
=> #<DateTime: 2022-01-01T00:00:00+00:00 ((2459581j,0s,0n),+0s,2299161j)>

datetime.to_s
=> "2022-01-01T00:00:00+00:00"
  • Date

基本形:
Date.strptime(str = '-4712-01-01', format = '%F', start = Date::ITALY)

date = Date.strptime('2022-01-01T00:00:00+00:00')
=> #<Date: 2022-01-01 ((2459581j,0s,0n),+0s,2299161j)>

date.to_s
=> "2022-01-01"

Timeクラスのインスタンスメソッド iso8601

XML Schema で定義されている dateTime として表現される形式の文字列を返す。

t = Time.strptime("00000001122022", "%S%M%H%d%m%Y") # => 2022-12-01 00:00:00 +0900

# Timeクラスのインスタンスに対して呼び出すとフォーマットされた文字列が返る
t.iso8601 # => "2022-12-01T00:00:00+09:00"

Timeのクラスメソッド iso8601

XML Schemaで定義されているdateTimeとしてdateをパースしてTimeオブジェクトに変換する。
dateがISO8601で定義されている形式に準拠していない、
またはTimeクラスが指定された日時を表現できないときにArgumentErrorが発生。

# 引数にdatetimeの文字列を指定し、Timeオブジェクトを生成
t = Time.iso8601(DateTime.now.to_s) # => 2022-11-19 17:30:46 +0900
t.class # => Time

Forwardableモジュール

クラスに対し、メソッドの委譲機能を定義するモジュール。

委譲元のオブジェクトでaliが呼び出された場合に、委譲先のオブジェクトのメソッドへ処理が委譲されるようになる。
委譲元と委譲先のメソッド名が同じ場合は,aliを省略することが可能。

def_delegatordef_instance_delegatorのエイリアス。
includeではなくextendして使う点に注意。
以下の1と2は同じ意味

# 1
def_delegators :@records, :size, :<<, :map
# 2
def_delegator :@records, :size
def_delegator :@records, :<<
def_delegator :@records, :map

パターンマッチ

case whenと似てるけど違う。

case 
in
in
else

大変詳しく解説されているのでこちらをぜひ。

他にも思い出したら書き足そうと思います。

ポイントまとめ

Goldを勉強しながら整理するために書いたメモです。
試験前に見返してました。

数値を表す組み込みクラス

Numericをスーパークラスとして以下のサブクラスがあります。

Numeric.subclasses => [Date::Infinity, Complex, Rational, Float, Integer]
クラス名 内容
Numeric 数値を表す抽象クラス
Integer 整数を表すクラス
Float 少数を表すクラス
Rational 分数で表せる数値のクラス(有理数)
Complex 虚数iを含めたクラス(複素数)
Date::Infinity 無限の日付を扱うクラス

演算の戻り値クラス

ここは混乱しがちなので、Integer < Rational < Float < Complex の順で強いと覚えました。

演算 戻り値クラス
IntegerとRationalの演算 Rational t = 1 + 3r
t.class # => Rational
FloatとRationalの演算 Float t = 1.0 + 3r
t.class # => Float
FloatとIntegerの演算 Float t = 1.0 + 3
t.class # => Float
IntegerとComplexの演算 Complex t = 1 + 3i
t.class # => Complex
FloatとComplexの演算 Complex t = 1.0 + 3i
t.class # => Complex
RationalとComplexの演算 Complex t = 3r + 1i
t.class # => Complex
Date同士の減算 Rational t = Date.new(2022,12,25) - Date.new(2022,12,24)
t.class # => Rational
DateTime同士の減算 Rational t = DateTime.new(2022,12,25) - DateTime.new(2022,12,24)
t.class # => Rational
Time同士の減算 Float t = Time.new(2022,11,1,10,00) - Time.new(2022,11,1,9,00)
t.class # => Float

requireとloadの違い

require load
同じファイルを1回のみロード 無条件にロードする
拡張子(.rbや.so)を自動補完する 拡張子を補完しない
主にライブラリのロードに使用 主に設定ファイルの読み込みに使用

rubyのオプション

  • -t-fはrubyのオプションに存在しない

Rationalオブジェクト

作り方は以下の3パターン

  1. 1 / 3r
  2. 1 / 3.to_r
  3. Rational(1, 3)

変数

クラス変数(@@で始まる)

  • スーパークラス・サブクラスで共有される。
  • クラスメソッド内・インスタンスメソッド内で共有される
  • 定義されていなければ呼び出してもNameErrorになる

インスタンス変数(@で始まる)

  • インスタンスを生成した際にデータが作成される。
  • インスタンスが生成されていないとnilになる。
  • スーパークラス・サブクラスで共有される。

クラスインスタンス変数(@で始まる)

  • インスタンスの生成とは無関係に、クラス自身が保持しているデータ。
  • クラス構文の直下やクラスメソッドの内部で@から始まる変数を操作するとクラスインスタンス変数にアクセスすることになる。
  • スーパークラスとサブクラスで共有されない
  • インスタンスメソッド内で共有されない

LambdaとProc

Proc.newの引数の数は曖昧
lambdaの引数の数は厳密

procの記法

Proc.new {|...|}
proc {|...| }

lambdaの記法

->(...}{ }
lambda { |...| }

aliasを定義する時の記法

# カンマ不要
alias new_method old_method
alias :new_method :old_method
alias $new_method $old_method

# カンマが必要、文字列かシンボルのみ
alias_method "new_method", "old_method"
alias_method :new_method, :old_method

定数探索

  • レキシカルスコープ→スーパークラスの順に探索する
  • ネストされている時は内側から外側に順番に探索
  • トップレベルの定数から探索したい時は左辺に::を付ける

メソッド探索

  • 基本的には自クラス→なければスーパークラスを探索する
  • prependされているモジュールがある時
    prependされたモジュールのメソッド→自クラスのメソッド→スーパークラスのメソッド
  • includeされているモジュールがある時
    自クラスのメソッド→includeされたモジュールのメソッド→スーパークラスのメソッド

Refinement

  • usingを定義するとスコープ内でのみ有効。スコープを抜けると無効になる。
  • usingより後に同名のメソッドが定義されたとしてもusingが有効なスコープ内である限りusingが使用される
  • メソッド内ではusingは定義できない

undef(未定義)

# 識別子 or シンボル
undef hoge
undef :hoge
# シンボル or 文字列
undef_method :hoge
undef_method "hoge"

メソッドをundefした場合の挙動

  • undefを呼び出したクラスとそのサブクラスで未定義にしたメソッドの呼び出し不可になる
  • スーパークラスのメソッドには影響しないが、サブクラスでそのメソッドが「未定義」となっているのでスーパークラスの同名メソッドを探索しない。よってスーパークラスに同名メソッドがあってもNoMethodErrorで終わる。

例外処理

NoMethodError < NameError < StandardError

NoMethodErrorのスーパークラスはNameError
NoMethodErrorが発生する場面でNameErrorrescueしていたらそこでresuceされる。

begin
  "example".test
rescue NameError => e # ここで捕捉される
  puts "NameErrorで捉えました: #{e.class}"
rescue => e # こっちではない
  puts "StandardErrorで捉えました: #{e.class}"
end

=> NameErrorで捉えました: NoMethodError

ancestors

  • クラス、モジュールのスーパークラスとインクルードしているモジュールを優先順位順に配列に格納して返す。
  • レシーバがクラスかモジュールの場合のみ有効。
  • レシーバがインスタンスの場合はNoMethodErrorになる。

組み込みライブラリ

$0はファイル名、$1以降はグループ化された正規表現にそれぞれマッチした値を返す

%r|(http://www(\.)(.*)/)| =~ "http://www.example.com/"

p $0 => ファイル名
p $1 => "http://www.example.com/"
p $2 => "."
p $3 => "example.com"
p $4 => nil

splat演算子の*と可変長引数の*

  • splat演算子の*は配列を展開する
  • 可変長引数の*は引数に配列を指定する時に用いる

splat演算子

  • 配列を展開する
def add(x,y)
  x + y
end

p add(*[3,2]) # 引数が展開されてadd(3,2)と同じになる
# => 5

可変長引数

  • 引数の数が決まっていない場合に使う
  • 複数は指定できない
  • 他の引数がある場合、可変長引数は最後にしか指定できない
  • メソッドは引数なしでも呼び出せる
def test(*args) 
  args.map(&:itself)
end

# 引数なしで呼び出してもOK, 空配列が返る
p test
=>[]

p test(3,2)
=> [3, 2]

p test([1,2,3,4])
=> [[1, 2, 3, 4]]

p test([1,2],[3,4])
=> [[1, 2], [3, 4]]

出題された問題

覚えている範囲で似たようなコードで記載します。
新試験ならではの新しい問題と、旧試験から出ているであろう問題を分けてみました。

新試験からの出題傾向

filter_map

filter_mapメソッドの結果を選択肢から選ぶ問題です。
難しいものではなく、真の値だけが返ることを押さえていれば答えられました。

array = [30.4, -1.3, 2.76, 1/2r]
p array.filter_map {|n| n >= 0 && n.round }

[実行結果]
# ここを問う問題

[30, 3, 1] ←正解の選択肢
[30, -1, 3, 1] ←誤りの選択肢

パターンマッチ

パターンマッチの基本的な記法を問う問題が2問ほど出ました。
1つは公式模擬問題と同じ感じでwheninの選択肢からinを選ぶ問題。

もう1つはinの後ろの記法を問う問題でした。

case {name: 'Bob', age: 20}
in _(1)_ # ここを問う問題
  puts "#{name} is #{age} years old"
end

選択肢にはこういう感じのがあり、そういう書き方もあるのか・・?と一瞬迷いましたが1つ選択だったので特に難しくはなかったです。
{:name => name, :age => age} ←誤りの選択肢
{name: name, age: age} ←正解の選択肢

ナンパラ

Hashを使った番号指定パラメータの問題が出ました。

h = {a: "gold", b: "silver"}
puts h.map { puts "#{_1_}, #{_2_}"} # ここを問う問題

[実行結果]
a, gold
b, silver

_1_2の部分を選択肢から選ぶ感じだったと思います。

1: _1, 2: _2 ←正解の選択肢
1: $1, 2: $2 ←誤りの選択肢

Timeクラスのインスタンスメソッドiso8601

iso8601で変換したフォーマット形式を当問題でした。

t = Time.strptime("00000024021993", "%S%M%H%d%m%Y")
puts t.iso8601

[実行結果]
# ここを問う問題

こんな感じの選択肢があった気がします。
1993-02-24T00:00:00+09:00 ←正解の選択肢
1993-02-24T00:00:00z ←誤りの選択肢

旧試験からの共通の出題傾向

Refinement

Refinementが有効になっている時に同名メソッドを他のメソッドから呼び出す際、どちらのメソッドが呼ばれるのかを問う問題。

class A
  def foo
    puts "A"
  end

  def bar
    foo
  end
end

module M
  refine A do
    def foo
      puts "B"
    end
  end
end

x = A.new
x.foo # => A
using M
x.foo # => B
x.bar # => _(1)_ # ここを問う問題

「...!?」ってなりましたがAでした。
見直した結果Bにしてしまい間違えました。。。

メソッドの内部で呼び出されているメソッド

printメソッドの内部で呼び出すメソッドを問う問題が出ました。

class ShoppingList
  def initialize
    @items = []
  end

  def add_item(item)
    @items << item
  end

  def _(1)_ # ここを問う問題
    @items.map { |e| "- #{e}" }.join("\n")
  end
end

list = ShoppingList.new

list.add_item("Milk")
list.add_item("Bread")
list.add_item("Eggs")

print list

公式模擬問題の中でpputsは出てたので覚えていたのですが、printが出てinspectto_sどっちだっけ。。。となり間違えました。
printto_sが呼ばれますのでto_sをオーバーライドすることでカスタマイズできます。

メソッド 呼び出すメソッド
p inspect
puts to_s
print to_s

メソッドのオーバーライド

class Sample
  def _(1)_(name, *args) # ここを問う問題
    puts "Ruby #{name}"
  end
end

s = Sample.new
s.ruby
s.test

[実行結果]
Ruby ruby
Ruby test

define_method, const_missing, method_missing(とあと何か1つ忘れた)から選ぶ問題でした。
答えはmethod_missingです。

ヒアドキュメント

ヒアドキュメントのリテラルを問う問題です。
出力された結果から、最初のコードをこんな感じの選択肢から2つ選べというものでした。

  1. <<~EOS
  2. <<EOS.gsub(/ /, "")
  3. <<-EOS.gsub(/ /, "")
  4. <<-EOS
a = _(1)_ # ここを問う問題
    Ruby Gold
    version 3
  EOS
puts a

(実行結果)
Ruby Gold
version 3

1はすぐ選べたのですがヒアドキュメントにgsubメソッドを使っての記法は初めて見たので迷いました...
答えは1と3です。
<<EOSは末尾をインデントできないので2はエラーになり、4の場合は出力結果が異なる(インデントされた状態で結果が出力される)ので誤りです。
こういう初めて見る問題も落ち着いて考えればわかるものが多かった気がします。

その他

  • FloatとRationalの演算で結果のクラスが何になるかを問う問題
  • 実行結果からsplat演算子と可変長引数の位置を問う問題
  • 親クラスのprivateメソッドをサブクラスでpublicに変更できるかの問題
  • 例外処理(複数出た)
  • 正規表現(複数出た)
  • Proc, lambda(複数出た)
  • レシーバのclasssuperclassは何かを問う問題
  • 例外を表すクラスに共通するスーパークラスは何かを問う問題(Exception)
  • ENVの記法を問う問題 (ENV['test']という感じで文字列)
    など

出なかった問題

  • class_eval, instance_eval
  • Fiber
  • Thread
  • Rubyオプション
    など

出題傾向の割合

完全に感覚ではありますが、こんな感じの割合になりそうです。

  • 新試験から新しく出題されている問題で、かつ難易度が公式模擬問題と同等レベルの問題 : 20%
  • 旧試験の時から出ている問題で、かつ難易度が合格教本やRExレベルの問題 : 70%
  • 旧試験の時から出ている問題で、合格教本やRExで見かけない(難しいor珍しい)問題 : 10%

何を間違えたかは不明なのですが、自信がない問題はほとんど旧試験の時からの出題範囲のものでした・・・。
逆に新試験からの出題範囲はひねった問題は特になく、公式模擬問題からそのままだったり基本的な文法だけだったので「これ知らないんだけど!?」っていう驚きの問題はなかったです。

Ruby技術者認定試験を受けた感想

Silverの時は直前まで暗記を必死にした記憶があり、短期記憶に頼った部分も多かったように思います。
覚える量で言えばSilverの方が多かった印象です。

Goldはロジックを理解して答えることが求められるため、記憶力はそこまで使わなかったように思います。
実務で曖昧だった部分をまとめて学ぶことができ、コードの可読性が上がり見通しが良くなったと思います。
あとはお恥ずかしながらRubyとRailsを混同していた部分もあり、「これってRailsじゃなくてRubyのメソッドだったんだ」といった発見もあったりして面白かったです!

試験時間自体は90分あって余ると思うので、余った時間に見直しが必須です。
特に問題文をよく読み直して、「適切なものを選択」「不適切なものを選択」「2つ選択」などを読み間違えないように注意しましょう。
どうしても分からない問題は一旦仮で答えて旗を立ててすぐ次の問題に行くのが良いです。
一通り解いてから見直しに入ると、他の問題からヒントを得てさっき分からなかったのが分かるようになっていたりもするので見直しに時間をかけた方が良さそうです。

反省点

時間の兼ね合いもあり試験前にはできなかったですが、これをやっておけばさらに良かったかもと思う点を挙げてみます。
ここまでやっておけば恐らくもっと安心感のある点数が取れそうな気がします。

  • メタプログラミングRubyを読む(メタプロ的な理解が浅かった)
  • チェリー本の改訂版を読む(改訂前のチェリー本+ @jnchito さんのQiitaを大変参考にさせていただきました)
  • RExで安定して9割以上取れるようにしておく(8割後半くらいが平均だった)

最後に

しっかり対策していけば新しい出題範囲も怖がる必要はなさそうです。
この記事を読んで少しでも受験するモチベーションやきっかけになれば大変幸いです。

参考にさせていただいた記事等

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
6