プログラミング漫遊記

思ったことや、勉強したことをつらつらと。

【Ruby】二次元配列についての色々な操作を考えてみた#1(最大文字数取得)

こんにちは。はるぐちです。フィヨルドブートキャンプで学習中です。 今回は二次元配列について考えてみました。 普通の配列については以前こんな記事を書いたので参考にしてください。

haruguchi-yuma.hatenablog.com

二次元配列って?

二次元配列とは配列の中に入れ子になって配列がある配列(何言ってるかよくわからん)です。ネストの深さによっては「多次元配列」や「多重配列」と言ったりもしますが正式な名前がなんなのかは知りません。教えてください。

たとえばこんな感じ。

# 二次元配列
[ [  ,  ], [  ,  ], [  ,  ,  ] ]

ネストが深くなればなるほど自分が今どんな操作をやっているのかわからなくなってくるので難しいですよね。 この二次元配列についていろんな操作を考えてみたいと思います。 書き出してみたら思いのほか長くなったので、何回かに分けて記事にしたいと思います。記事稼ぎです。

1.配列の中の文字列に対して最大の文字数を取得する

こんな二次元配列を用意します。

word_groups = [
  ["monkey", "gorilla", "orangutan"],
  ["hawk", "woodpecker", "goose"],
  ["dlphin", "shark", "haruguchi"]
]

この中から最大の文字数を取得します。期待される結果はwoodpeckerの文字数で10です!

結論

まずは結論から。アプローチを2つ考えました。

  1. ループを回しながら探していくパターン
  2. 配列を平坦化して考えるパターン
# ループを回しながら探すパターン
word_groups.map{ |words| words.map(&:size).max }.max #=> 10

### 配列を平坦化するパターン
word_groups.flatten.map(&:size).max #=> 10

解説

メソッドチェーンが多くてわかりにくいのでいくつかのステップに分けて考えていきます。

ループを回すパターンの方

word_groups.map{ |words| words }
#=> ["monkey", "gorilla", "orangutan"],
#=> ["hawk", "woodpecker", "goose"],
#=> ["dlphin", "shark", "haruguchi"]

1回目のmapでは配列の要素(内側の配列)を1つずつ取り出しています。この内側の配列に対してmapメソッドを使って要素の('monkey'とかの文字列)文字数を確認していきます。

word_groups.map{ |words| words.map(&:size) }
#=> [6, 7, 9]
#=> [4, 10, 5]
#=> [6, 5, 9]

これで各配列での文字数が取得できました。 次に、その配列に対して最大値をとってきます。最大値を返すメソッドはmaxメソッドです。maxメソッドの場所に注意します。

word_groups.map{ |words| words.map(&:size).max }
#=> [9, 10, 9]

できました。 次に、内側の配列に対して最大値を取得したいので、maxmapの内側に記述します。 これでそれぞれの配列での最大文字数が取得できました。 あとはその中からさらに最大値をとってくるだけです。

word_groups.map{ |words| words.map(&:size).max }.max
#=> 10

慣れるまではイメージしづらいかも知れません、、、

配列を平坦化するパターン

こちらはもっとシンプルな考え方になります。 まずflattenメソッドを使って二次元配列のネストをとっぱらい、普通の配列に変換します。

word_proups.flatten
#=> ["monkey", "gorilla", "orangutan", "hawk", "woodpecker", "goose", "dlphin", "shark", "haruguchi"]

次に、文字列を文字数に変換します。

word_groups.flatten.map(&:size)
#=> [6, 7, 9, 4, 10, 5, 6, 5, 9]

それぞれの単語の文字数が出ました。そして、その中から最大値を取得すれば終わりです。

word_groups.flatten.map(&:size).max
#=> 10

ループを回すより考えやすかったと思います!

次回は値が入った二次元配列の合計値を計算するです。

【Node.js】CLI版のメモアプリを作って学んだ難しい課題への向き合い方

こんにちははるぐちです。フィヨルドブートキャンプ(以下フィヨルド)でメモアプリを作成したのですが、はっきりいって

めっちゃむずかしかった、、、

正確には「今回もちゃんと難しかった」という感想になるのですが、「何をどうすればいいのか?」というレベルで難しいと感じたのはRubyでlsコマンドを実装するとき以来だったのでびっくりしました。JavaScriptという言語に慣れていないというのも原因かもしれません。

しかし、今回に関しては「全く意味わからん」状態ながらも精神的には楽しんでできたので、その学びから難しい問題にどう立ち向かっていけばいいのか書ければいいなと思います。

注意:課題は提出しましたが、現時点ではレビューはいただいておりませんので技術的なことは書かないつもりです。なのでネタバレはありません。

注意:難しいと煽ってはいますが、実際はフィヨルドでは助け合いのコミュニティがありますので質問すれば丁寧に答えてくれますし、だいたいみんな楽しんで苦しんでいます(多分)!

対象者

  • プログラミングが難しい〜!ってなってる人
  • プログラミング難しすぎて夢に出てきたことがある人
  • プログラミングが難しすぎて晩御飯喉を通らなくなった人
  • フィヨルドブートキャンプって知ってるけど、カリキュラムどうなの?って思っている人

課題の位置付け

まずはどういうプラクティスなのか説明します。 フィヨルドではHTML・CSSからプラクティスが始まり、今回のメモアプリは後半のプラクティスであるJavaScriptの課題「クラス構文について知ろう」というプラクティスの課題でした。メモアプリ自体はすでに作ったことがあって、1回目はSinatraを使ってメモをファイルに保存する形式、2回目は同じくSinatraで作るのですがメモをデータベースに保存する形式です。今回は言語がRubyからJavaScriptに変わって3回目のメモアプリ作成となります。DLI(コマンドラインインターフェース)版を作るのは初めてなのでいったい何をどうすればできるのかわからないところからの実装になりました。

ちなみに、プラクティスを順番にやっていくとこんな感じ。85%かぁ。(随分遠いところまで来たもんだ) 学習日数はかなりかなり遅い方なので参考にしないでください(途中7、8ヶ月ほどやっていない時期がありました。)

f:id:haruguchi_yuma:20210829155342p:plain
ラクティスの進捗状況

学習カリキュラムについては公開されていますので、https://bootcamp.fjord.jp/practicesを参考にしてください。

難しいに立ち向かう

1.難しいの解像度を上げる(わからないの言語化)

ここからが本題です。

難しい。」と、一言でいうのは簡単ですが、もっと解像度をあげなければ問題は解決しないと考えました。 ということで、事実ベースでわかることわからないことを書き出していきます。ここで個々の難易度は気にしません。思いつくままに書き出していきます。

【事実】

  • メモアプリはNode.jsを使って実装する←Node.jsの環境は整っている
  • 標準入力を受け取る←現段階で方法がわからない <調べる>
  • オプションを受け取る←一つ前のプラクティスでオプションの受け取り方は学習済み、つまりわかる(minimistを使う)
  • sqlite3を使う←よくわからない <調べる>
  • メモを選択できるようにする←わからない <調べる>
  • クラス構文を使ってクラス化する←構文自体は知っている
  • クラス設計←Rubyと同じ感じならできると思う

2.簡単なところから順番に調べていく

いきなり、アプリを完成させようとしても全体像が掴めていないことがほとんどなので、先ほど列挙したわからないことを1つ1つ地道に調べていきます。 難しいところから調べていくと心が折れるのでなるべく簡単(だと感じる)なところから手をつけていき、それでもわからないことがあれば、引き続き書き出していきます。ここで、おすすめなのは公式ドキュメントから順番に調べるということです。

僕は、最初はQiitaなどのわかりやすい記事を読んでわかったつもりになっていたのですが、これは遠回りだったように感じます。全体のうちの一部しか解説されていないことも多く、使い方があっているか間違っているか自分では判断がつきません。まずは公式ドキュメントなどをみて、そのライブラリでできることの全体像を入れてからQiitaなどの記事を見た方が「どうしてそういった書き方をしているのか?」など細かいことがわかると思います。だた、公式ドキュメントは難しくて理解できないこともあります。そこが一番辛かったかもしれない。

今回readlineenquirersqlite3などのモジュールを調べたのですが、ドキュメントが英語だったのでとても時間がかかりました。とはいえ今はDeepLなどの優れた翻訳があるので時間はかかりましたが力になったように思います。

  • 標準入力を受け取る ←readlineというモジュールを使えばいいとわかった
    • readlineのドキュメントを調べる
      • <具体的な使い方>hogeがわかった
      • <具体的な使い方>fooがわかった
      • <具体的な使い方>barはよくわからん ←<調べる>

3.それでもわからなければ、基礎・基本に戻る(戻ることを恐れない)

先ほどさらっと公式ドキュメントを読めばいいと書きましたが、実際は公式ドキュメントが全然読めませんでした。 じゃあサンプルコードだ!「ん?何やってるか全然わからん!」

あるあるです。

読めないし、書けないということはそもそもJavaScriptについての基本的な文法が理解できてないのだと思い、基本的な構文を解説しているサイトや書籍で学習するところまで戻っていきました。 どの力が不足して公式ドキュメントが読めないか分かればその部分だけ学習しなおせばいいのですが、僕の場合はわからなかったので一通り学習しました。結果的によかったと感じています。

4.「分かった」から「できる」へ(小さく実験する・条件を変えて試す)

公式ドキュメントが読めるようになって「分かった」の状態になったら、実際に手を動かして「できる」の状態にします。どちらかというと公式ドキュメントを読みながら手を動かすのでほぼ同時進行かもしれませんが、実際に手を動かさないとできるようにはならないのでここはとても大事なところだと思います。

また、小さく実験するというのがポイントだと思います。

FuzzBuzz問題で例えると、いきなり「3で割り切れる場合」と「5で割り切れる場合」と「15で割り切れる場合」を考えるのではなく、まずは「3で割り切れる場合」だけを考えよう。と言った具合に小さく動かしていき、それがクリアできたら「5で割り切れる場合」を考えようとステップアップしていくのがいいと思います。

もう一つのポイントは条件を変えて試すということです。

これまたFizzBuzz問題で例えると、「3で割り切れる場合」という条件を「7で割り切れる」場合に変えたり、3つの条件から4つの条件にしたときにプログラムが自分の意図する挙動になっているか確かめるということです。条件を変えて色々試すことで本当に理解できているかの確認になりました。

5.あとはひたすら繰り返し

あとは1~4を繰り返しながら進めていきます。 進めていく中でどうしても理解できない、わからないところは友達・メンターなどに質問するといいです。

フィヨルドブートキャンプではその他に、Q&AやDiscordなどでわからねぇ!と叫べるコーナがありおすすめです。(宣伝)

以上、メモアプリを通して学んだ難しい問題への立ち向かい方でした。

【Ruby】ハッシュを使って条件分岐を減らそう!

こんにちは「はるぐち」です。フィヨルドブートキャンプで学習しています。 今回はある値によってたくさんの条件分岐をしたいとき、ハッシュを使うとシンプルになることを学んだので忘備録として記述しておきたいと思います。

想定

果物がたくさん入った配列があります(たくさんとは)。この中からランダムに1つ取り出したとき、値段が返ってくるブログラムを考えます。

仮にpriceメソッドとでも名付けましょう。 以下のような出力を期待します。(果物の値段は勘で決めました)

fruits = ['apple', 'banana', 'melon', 'pineapple']

price(fruits.sample) # ランダムに1つ選ぶ
#=> apple: 120円
#=>とか
#=> banana: 60円
#=>とか
#=> melon: 3000円
#=>とか
#=> pineapple: 800円
#=> となる

if文を使う場合

まずはif文*1で書いていきます。 elsifでの条件分岐が多いので読みにくい感じがします。

fruits = ['apple', 'banana', 'melon', 'pineapple']

def price(fruit)
  price = 
    if fruit == 'apple'
      120
    elsif fruit == 'banana'
      60
    elsif fruit == 'melon'
      3000
    elsif fruit == 'pineapple'
      800
    end
  puts "#{fruit}: #{price}"
end

price(fruit.sample)

果物が増えたとき、elsif fruit == 果物 値段と記述しないといけないためかなり面倒です。 そして何回も書くfruit ==が結構しんどい。あーなくしたいなぁ〜。

case文を使う場合

特定の値による条件分岐が多い場合はcase文*2を使うことも多いと思います。

fruits = ['apple', 'banana', 'melon', 'pineapple']

def price(fruit)
  price =
    case fruit
    when 'apple'
      120
    when 'banana'
      60
    when 'melon'
      3000
    when 'pineapple'
      800
    end
  puts "#{fruit}: #{price}"
end

price(fruits.sample)

if文のときと比べると、fruit ==の部分がないので少しだけ、本当に少しだけスッキリはしました。 また、値段を戻り値とするだけの処理なのでthenを使って1行で記述してもいいような気がします。

fruits = ['apple', 'banana', 'melon', 'pineapple']

def price(fruit)
  price =
    case fruit # thenを使って1行で記述
    when 'apple' then 120
    when 'banana' then 60
    when 'melon' then 3000
    when 'pineapple' then 800
    end
  puts "#{fruit}: #{price}"
end

price(fruits.sample)

ただ、この場合も果物が増えるごとにwhen 果物 then 値段という文を増やさないといけません。

そこで、ハッシュを使ってみます。

ハッシュを使う場合

ハッシュを使うと条件分岐が消え、果物を追加するときも楽に追加することができます。 つまり、管理も簡単ということです。

fruits = ['apple', 'banana', 'melon', 'pineapple']

def price(fruit)
  price = 
  {
    'apple' => 120,
    'banana' => 60,
    'melon' => 3000,
    'pineapple' => 800
  }[fruit]
  puts "#{fruit}: #{price}"
end

price(fruits.sample)

引数として受け取った果物がハッシュのキーとなって値段を返しています。 条件分岐が消えてシンプルで見やすくなりましたし、果物を追加するのも簡単です。

また果物と値段のhashを使い回すときは定数として定義しておくのもいいかも知れません。

fruits = ['apple', 'banana', 'melon', 'pineapple']

FRUITS_TABLE = {
  'apple' => 120,
  'banana' => 60,
  'melon' => 3000,
  'pineapple' => 800
}

def price(fruit)
  price = FRUITS_TABLE[fruit]
  puts "#{fruit}: #{price}"
end

price(fruits.sample)

まとめ

  • ハッシュを使ったら条件分岐をなくすことができる。
  • 条件の変更や条件そのものを増やしたり減らしたりすることも簡単!

つまり、ハッシュすごい!

もっといい方法があればぜひ教えてください。

*1:正確にはif式ですがここではif文で統一しています。そのほかも同じ扱いです。

*2:*1に同じく

【Ruby】ボウリングプログラムで気づいたeach_with_objectの便利さ!

フィヨルドブートキャンプではプラクティスとしてボウリングのスコア計算プログラムを作成します。提出物に合格をもらった後、他の受講生のコードを読んでいて初めて知ったメソッドにeach_with_objectというメソッドがあります。これはめちゃくちゃ便利じゃね?(自分が知らなかっただけ)と思った次第ですので、忘備録として、また学習ノートとしてeach_with_objectの使い方を記しておきたいと思います。フィヨルド生には若干のネタバレにはなるかもしれませんが、あんまり影響しないよう断片的なコードにしています。

ボウリングのスコア計算プログラムとは

実行例は以下の通りターミナルでコマンドラインの引数としてカンマ区切りのスコアを渡すと得点が出力されます。

$ ./bowling.rb '6,3,9,0,0,3,8,2,7,3,X,9,1,8,0,X,6,4,5'
139

上記でストライクは'X'として表されています。またスペアは3,7などの数字で合計10になるときです。 また、ボウリングはスコアの計算方法が改正されていますが、今回は現行のカレントフレームスコア方式ではなく、旧のスコア計算方法で実装しています。

詳しくはコチラ:ボウリングの新スコア計算方法!カレントフレームスコアシステムで遊んでみたよ

自分が書いたコード

def convert_to_value(score)
  shots = []
  score.split(',').each do |shot|
    if shot == 'X' && shots.size < 18
      shots << 10 << 0
    elsif shot == 'X'
      shots << 10
    else
      shots << shot.to_i
    end
  end
  shots
end

上のコードで記述したメソッドは引数で'6,3,9,0,0,3,8,2,7,3,X,9,1,8,0,X,6,4,5'といった文字列のスコアを受け取る想定をしています。 カンマで区切られている数字やマークを数値に変換し、一つの配列として出力しています。そのとき最終フレーム以外のストライクのマークX10,0として処理し、最終フレームのストライクのマーク'X'は単に10としています。

実際に引数を渡してみると以下のようになります。

convert_to_value('6,3,9,0,0,3,8,2,7,3,X,9,1,8,0,X,6,4,5')
=>  [6, 3, 9, 0, 0, 3, 8, 2, 7, 3, 10, 0, 9, 1, 8, 0, 10, 0, 6, 4, 5]

each_with_indexを使って書くと

def convert_to_value(score)
  score.split(',').each_with_object([]) do |shot, shots|
    if shot == 'X' && shot.size < 18
      shots << 10 << 0
    elsif shot == 'X'
      shots << 10
    else
      shots << shot.to_i
    end
  end
end

さて、何が変わっているでしょうか

- shots = []
- score.split(',').each do |shot|

+ score.split(',').each_with_object([]) do |shot, shots|
  • 【変更前】空の配列を用意し、条件に合うものをその配列に突っ込んでいました。

  • 【変更後】each_with_objectを使ったことにより、引数に空の配列を渡すことで、ブロック変数の第二引数であるshotsにセットされます。

- shots
end

また【変更前】は最後にshotsを記述することで配列shotsを戻り値としていたのですが、【変更後】はeach_with_objectの戻り値がshotsなためコードがスッキリしました。

まとめ

空の配列(ハッシュ)を用意して、何らかの条件によって配列に入れていくような操作はeach_with_objectが使える。またメソッドの戻り値は引数のオブジェクトになる。

つまり、配列の用意と戻り値で2行ほどお得!(ちょっと俗っぽい言い方)

参考:https://docs.ruby-lang.org/ja/latest/method/Enumerable/i/each_with_object.html

【Ruby】配列の作り方

複数のデータをひとまとめにしたいとき、配列を作ると思います。 そこで配列の作り方を自分なりにまとめてみました。

配列の作り方

[]を使う

# 空の配列を作る
[]
[].size # => 0
[].class # => Array

文字列を''で囲むように、[]を使って囲むと簡単に配列が作れます。 間にオブジェクトを入れないと空の配列ができて当然要素数は0です。 複数のオブジェクトを格納するには、,で区切って並べます。

# 複数のオブジェクトを格納する
ary = [1, 2, 3, 4, 5]

# 型が違うものでも格納できる
ary = [ 1, 'a', :b , { name: 'Alice, score: 40 }, [2, 3] ,[4, 5, 6], nil }

配列の中には、ハッシュや配列、nilなど異なるデータ型のオブジェクトも格納することができます。

Array.newを使う

Array.newを使うと配列を生成することができます。

Array.new # => []

Array.new(3) # => [nil, nil, nil]

Array.new(3. 'hello') # =>  ["hello", "hello", "hello"]

Array.newを使う場合、引数を指定しなければ空の配列が生成されます。また引数を1つ取る場合は指定された数のnilが格納され、第二引数を指定すると、指定したオブジェクトが格納されます。

ただし、この方法で生成されたオブジェクトは全て同一オブジェクトになるので、破壊的なメソッドを使うときは注意が必要です。

# []で作った配列は同じ文字列でも同一オブジェクトではない
ary1 = [ 'hello', 'hello' , 'hello' ]
ary1.map { |a| a.object_id }
=> [280, 300, 320]

# 3つのhelloは全て同一オブジェクトとみなされる
ary2 = Array.new(3, 'hello') # =>  ["hello", "hello", "hello"]
ary2.map { |a| a.object_id }
# => [260, 260, 260]

# 破壊的メソッドを用いるときは注意が必要
ary2[0].upcase! # => "HELLO"
ary2 # => ["HELLO", "HELLO", "HELLO"]

上の例では、配列の1つ目の要素を破壊的なメソッドのupcase!メソッドを使って大文字にしていますが、全ての要素が変更されてしまいます。

ちなみにブロックを使うと異なるオブジェクトが生成されます

ary3 = Array.new(3) { 'foo' } # =>  ["foo", "foo", "foo"]
ary3.map(&:object_id) # => [340, 360, 380]
ary3[0].upcase! # => "FOO"
ary3 # => ["FOO", "foo", "foo"]

%記法

%wで配列を作ることができます。その場合、要素の区切りは半角スペースになります。

 %w(apple banana melon)
# => ["apple", "banana", "melon"]

囲む記号は始めと終わりがあっていれば良い ()でも[]でも{}でも@でも$でも・・・
%w[apple banana melon]
# => ["apple", "banana", "melon"]

%w@apple banana melon@
# => ["apple", "banana", "melon"]

また、配列の要素をシンボルにしたいときは%iを使います

 %i(apple banana melon)
# => [:apple, :banana, :melon]

連番の配列を作る

1から10までの連番の配列を作る方法を考えます。

# 愚直に頑張る
[ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# 範囲オブジェクトを用いる
(1..10).to_a
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# times.mapを組み合わせる
10.times.map { |n| n + 1 }
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

# ブロックを使う ブロック変数にはインデックスが代入される
Array.new(10) { |n| n + 1}

応用として奇数の連番を作る方法を考えます。

# 愚直に頑張る
[ 1, 3, 5, 7, 9]
# => [ 1, 3, 5, 7, 9]

# 範囲オブジェクトを用いる
(1..5).to_a.map { |n| 2 * n - 1 }
# => [1, 3, 5, 7, 9]

# times.mapを組み合わせる
5.times.map { |n| 2* n + 1 }
# => [1, 3, 5, 7, 9]

# ブロックを使う ブロック変数にはインデックスが代入される
Array.new(5) { |n| 2 * n + 1 }
# => [1, 3, 5, 7, 9]

こう考えるとやっぱりmapメソッドは便利ですね!

他にもあれば、追加していきたいと思います。

クラスメソッドの中でプライベートメソッドは呼べない

前回アプトプットシリーズとしてRubyで「ビンゴカード作成問題」に挑戦したのですが、そのときにメソッドのことで勘違いがあったので忘備録として書いておきたいと思います。

haruguchi-yuma.hatenablog.com

どんなことを勘違いしていたのかというとクラスメソッドの中ではプライベートメソッドを呼べないということです。 言葉だけではわかりにくいので、例を使って示します。

勘違い

# こんなふうに勘違いしていた
class Bingo
  def self.generate_card
    hoge # privateメソッド呼び出し
  end

  private

  def hoge
    puts 'hoge'
  end
end

Bingo.generate_card
# => `generate_card': undefined local variable or method `hoge' for Bingo:Class (NameError)

ビンゴカードの生成をBingo.generate_cardというふうにクラスメソッドで呼び出したいと思いgenerate_cardというメソッドを定義しました。 最初はこのgenerate_cardメソッドに処理の全てを記述していたのですが、冗長性もあり共通部分を切り出したりメソッド化することに。

メソッド化した部分は外から呼び出されたくなかったので、privateキーワードを使ってメソッドを定義しています。

この状態で実行するとgenerate_card': undefined local variable or methodhoge' for Bingo:Class (NameError)`というエラーが出ます。 hogeっていうメソッドなんて定義されてないよ〜!と怒られてしまいます。

何がいけなかったのでしょうか?

原因 - なんでためだったのか

class Bingo
  def self.generate_card
    hoge # hogeのレシーバがBingoになっている
  end

  private

  def hoge
    puts 'hoge'
  end
end

クラスメソッドの中で呼び出されているhogeというメソッドのレシーバはBingoなので、クラスメソッドのhogeを探しにいっています。 当然クラスメソッドのhogeは定義していないので、NameErrorとなるわけです。

ではこれならどうだ - 書き方の工夫

クラスメソッドを探さないようにメソッドの中ででインスタンスを生成し呼び出してみます。

class Bingo
  def self.generate_card
    bingo = Bingo.new
    bingo.hoge 
  end

  private

  def hoge
    puts 'hoge'
  end
end

Bingo.generate_card
=> `generate_card': private method `hoge' called for #<Bingo:0x00007f817512be68> (NoMethodError)

ありがとうございます。はい。エラーです。 根本的にprivateメソッドを理解していないのが原因でした。

そもそもprivateメソッドってなんぞ?

『プロを目指す人のためのRuby入門』(以下チェリー本)をみてみます。

p.247

厳密にいうとpriveteメソッドは「レシーバを指定して呼び出すことができないメソッドになります。」

と書いてあります。ruby2.7より前のバージョンではselfがついている場合も呼び出せなかったのですが、Ruby2.7以降はselfの場合は呼び出せるようになっています。

ということでレシーバを指定して実験してみます。

class Sample
  def public_method_without_self
    private_method
  end

  def public_method_with_self
    self.private_method
  end

  private

  def private_method
    'プライベートメソッドが呼び出されたよ'
  end
end

sample = Sample.new
# レシーバを指定して呼び出す
sample.private_method
=> private method `private_method` called for #<Sample:0x00007fdd21a38440> (NoMethodError)

# レシーバなし(selfなし)で呼び出す
sample.public_method_without_self
=> "プライベートメソッドが呼び出されたよ"

# レシーバあり(self)で呼び出す Ruby2.7より前のバージョン
sample.public_method_with_self
=> private method `private_method` called for #<Sample:0x00007fdd21a38440> (NoMethodError)

# レシーバあり(self)で呼び出す Ruby2.7以降のバージョン
sample.public_method_with_self
=> "プライベートメソッドが呼び出されたよ"

ややこしいことは抜きにして一旦整理すると以下のことがわかります。

プライベートメソッドはレシーバ(selfを除く)を指定して呼び出すことができないメソッドである。

最終的に行きついた形

色々試行錯誤した結果以下のように落ち着きました。

class Bingo
  def self.generate_card
    new.generate_card
  end

  def generate_acrd
    hoge
  end

  private

  def hoge;end
end

Bingo.generate_card

今回のグダグダでprivateメソッドについて深く知ることができてよかったです。

参考

るりま https://docs.ruby-lang.org/ja/latest/doc/spec=2fdef.html#limit

Ruby2.7の変更点 サンプルコードでわかる!Ruby 2.7の主な新機能と変更点 Part 3 - 新機能と変更点の総まとめ

ブログ Ruby Classメソッドのアクセス権

ビンゴカード作成問題にチャレンジしてみた。

前提

フィヨルドブートキャンプ(以下、フィヨルド)というプログラミングスクールで学習しています。

フィヨルドでは、分野ごとにたくさんのプラクティスがあり、Rubyのプラクティスもそのうちの一つになります。 他にもDB設計のプラクティスがあったり、sinatraを使って簡単なWebアプリケーションを作るプラクティスがあったりするのですが、その間にどうしてもRubyことを忘れてしまうことがありました。

そこで、フィヨルドのDiscordで「rubyでアウトプット定期的にしていきたいけど、何やったらいいんだろうか?」と呟いたところ『プロを目指す人のためのRuby入門(通称チェリー本🍒)』でお馴染みのメンターの伊藤さんから以下のブログを紹介してもらったので、少しずつ解いていったものを忘備録も兼ねてブログに残していくことにしました。

blog.jnito.com

全部で何題挑戦するかはわかりませんが、まずはビンゴカード作成問題に挑戦しました。

ビンゴカード作成問題

概要

blog.jnito.com

  • ビンゴカードには1から75までの数字が記載される
  • 5x5の大きさで真ん中は穴が空いている
  • 列は左からB列、I列、N列、G列、O列となっている
  • 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

作ったプログラム

class Bingo
  def self.generate_card
    new.generate_card
  end
  
  def generate_card
    rows = []
    5.times do |i|
      range = ((1+i*15)..(1+14+i*15)).to_a
      rows << range.shuffle.first(5).map { |n| n.to_s.rjust(2)}
    end
    puts " B |  I |  N |  G |  O"
    format_cols(rows).map { |row| puts row.join(' | ')}
  end 

  private

  def format_cols(rows)
    rows[2][2] = '  '
    rows.transpose
  end
end

Bingo.generate_card

まずビンゴクラスを作成して、クラスメソッドからインスタンスメソッド(genarate_card)を呼び出しています。

列の整形

range = ((1+I*15)..(1+14+i**15)).to_a

この部分は各列の数の範囲を配列化したのもです。 iには0,1,2,3,4まで数字が入るので 実質、以下のような意味になっています。

range1 = (1..15).to_a
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15]
range2 = (16..30).to_a
# => [16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
range3 = (31..45).to_a
# => [31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45]
range4 = (46..60).to_a
# => [46, 47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
range5 = (61..75).to_a
# => [61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75]
rows << range.shuffle.first(5).map { |n| n.to_s.rjust(2)}

上記はshuffleメソッドで数字をランダムにシャッフルし、fisrtメソッドで先頭5つの数字の配列を作っています。 .map{ |n| n.to_s.rjust(2) }はrjustメソッドで2桁でけた揃えして右寄せにしています。その際文字列でないとrjustメソッドは使えないためto_sを使って文字列化しています。

ここはもっとスッキリ書けそうな気がするのですが、今現在の力ではこんなもんです。

p rows
# => [["15", " 8", " 6", " 4", " 7"], ["16", "23", "18", "25", "29"], ["35", "33", "37", "34", "31"], ["51", "50", "58", "60", "55"], ["74", "62", "63", "64", "61"]]

rowsはこんな感じで各列にあたる部分が1つの要素として収まっている状態です。

def format_cols(rows)
    rows[2][2] = '  '
    rows.transpose
  end

このメソッドは真ん中(N列)に穴を開け、transposeで行と列を入れ替えています。 実行例はこんな感じ

# 真ん中に穴を開けて行と列を入れ替える
p format_cols(rows)
[["10", "29", "41", "48", "66"], [" 5", "19", "34", "52", "73"], [" 1", "21", "  ", "60", "61"], ["12", "18", "33", "53", "69"], [" 3", "24", "38", "46", "72"]]

表示部分

puts " B |  I |  N |  G |  O"
format_cols(rows).map { |row| puts row.join(' | ')}

ヘッダー部分は素直に作って、あとは' | 'でjoinさせています。

感想

各列に範囲があったので、プログラムでそれをどう表すか悩みました。

もっとスッキリ書けそうな部分はあるのでしょうが、それは成長した未来の自分に任せることにします。 もし何かあればコメントで指摘してください。

BINGOカード作成問題はアウトプット10題の中では比較的簡単なプログラムだと思いますので是非挑戦してみてください!