freeeの開発情報ポータルサイト

『勝手にコードゴルフ王決定戦 in RubyKaigi 2024』を開催しました! - 後編

こんにちは。freee エンジニアの yongi です。

この記事は後編です。前編はこちらの記事をご覧ください。作問の背景や、当日の様子を紹介しています。後編では、問題の簡単な解説と、優勝回答の紹介をします。

developers.freee.co.jp

問題解説

詳しい内容については、GitHubリポジトリをご覧ください。回答者に求められているのは、沖縄の市町村の名前と座標、地図上での色の一覧を格納した配列から、地図を表す2次元配列を作成するコードを書くことです。

m = []
26.times do
  line = gets.split
  m << [line[0], "\e[#{line[1]}m", line[2].to_i, line[3].to_i]
end

# ここを回答

a.each { |row| puts row.map { |cell| "#{cell[1]}#{cell[0]}\e[0m" }.join }

問題は穴埋め形式になっており、地図の元となる市町村の情報は、下記のようにあらかじめ m という配列に格納されています。沖縄の市町村の個性的な名前を楽しんでいただける問題となっています。

伊江村 43 2 0
本部町 42 4 1
今帰仁村 43 5 1
国頭郡 41 7 1
名護市 45 5 2
大宜味村 42 6 2
東村 43 7 2
恩納村 41 3 3
金武町 42 4 3
宜野座村 43 5 3
読谷村 45 2 4
沖縄市 43 3 4
北谷町 41 2 5
北中城村 42 3 5
うるま市 45 4 5
浦添市 42 1 6
宜野湾市 45 2 6
中城村 43 3 6
那覇市 43 1 7
西原町 41 2 7
豊見城市 43 0 8
南風原町 42 1 8
与那原町 45 2 8
糸満市 42 0 9
八重瀬町 41 1 9
南城市 43 2 9

地図を描画するコード(9行目)は、 下記のように、地図上に表示する文字と色が、a という二次元配列の適切な位置に格納されていることを期待しています。見やすいように簡略化していますが、実際には a の要素は文字ではなく配列であり、色の情報も含んでいます。

    伊江
    村
        本部今帰  国頭
        町 仁村  郡
          名護大宜東村
          市 味村
      恩納金武宜野
      村 町 座村
    読谷沖縄
    村 市
    北谷北中うる
    町 城村ま市
  浦添宜野中城
  市 湾市村
  那覇西原
  市 町
豊見南風与那
城市原町原町
糸満八重南城
市 瀬町市

色も合わせてコンソールに出力すると、次のようになります。

コンソールに出力される沖縄の地図
コンソールに出力される沖縄の地図

出題者側では、次にような回答を想定していました。 m に対してループを回して、 a を構築しています。

a = Array.new(20) { Array.new(16) { [" ", ""] } }
# configure the map
m.each do |mapping|
  name = mapping[0] + "  "
  row = mapping[3] * 2
  col = mapping[2] * 2
  color = mapping[1]

  # Place the string in a 2x2 block
  a[row][col] = [name[0], color]
  a[row][col + 1] = [name[1], color]
  a[row + 1][col] = [name[2], color]
  a[row + 1][col + 1] = [name[3], color]
end

このコードはサンプルとしてリポジトリに含まれており、出題者側では 136 bytes まで短縮することに成功していました。配列の初期化を簡略化する、変数名を1文字にする、 product メソッドを用いる等の工夫をしています。

a=Array.new(20){["  ",""]*16};m.map{|p|n,r,d,c=p[0]+"  ",p[3]*2,p[2]*2,p[1];[0,1].product([0,1]){|i,j|a[r+i][d+j]=[n[i*2+j],c]}}

優勝回答

今回の優勝者は akouryy さんでした。なんと 73 bytes まで短縮していただきました!これだけ短ければ、暗記していつでも沖縄の地図を出力できそうです。

a=[];4.times{|k|m.map{(a[_4*2+k/2]||=[S=? ]*16)[_3*2+k%2]=_1[k]||S,_2}}

akouryy さんには景品としてサンダル、Tシャツ、バンダナ、エコバッグの4点セットを差し上げました。おめでとうございます!

サンダルTシャツバンダナエコバッグ
サンダル、Tシャツ、バンダナ、エコバッグ

ちなみに2位は Yusuke Endoh さんで、74 bytes の回答でした。1位との差はわずか 1 byte という接戦が繰り広げられました。 gist.github.com

a=[];4.times{|i|m.map{(a[_4*2+i/2]||=[? ]*16)[_3*2+i%2]=_1[i]||? ,_2}}

ほとんど同じコードですが、コード中に2回登場する ?  を変数に格納するかどうかが決め手となりました。

接戦の様子

これらの回答には、筆者にとってはあまり馴染み深くない3つの Ruby の記法が用いられており、大変勉強になったので、簡単に紹介させていただきます。

Numbered Parameters

ブロックパラメータを、前から順に _1, _2 , … として、名前をつけずに参照できる機能です。

一般的なブロック

m.map { |v| puts v[0] + v[1] + v[2] }

Numbered Parameters を使った場合

m.map { puts _1 + _2 + _3 }

? による文字リテラルの定義

? の後に文字を1字指定した場合は、その文字を表す文字列を返すという記法です。回答中では、?  という部分で、全角スペースを定義するために使われています。 ""で囲む場合に比べて1 byte 節約できます。

配列の括弧の省略

配列の括弧は省略できます。回答中では、末尾の ? ,_2 という部分で、Numbered Parameters と合わせて [" ", _2] という配列を定義するために使われています。

おわりに

freee のコードゴルフに参加していただいた皆さま、本当にありがとうございます!初めての開催で出題者側としてはドキドキでしたが、楽しんでいただけたでしょうか?今回紹介したもの以外にも、X 上でたくさんの回答をいただきましたので、ぜひチェックしてみてください。

それでは、また皆さんと Ruby で語り合えることを楽しみにしています!