ブログ「hp12c」の一問題(Ruby)

またまた Ruby ブログ「hp12c」からの問題(?)です。
melborne.github.io
さて、データ

data = <<EOS
player gameA gameB
Bob    20    56
Ross   68    33
Bob    78    55
Kent   90    15
Alice  84    79
Ross   10    15
Jimmy  80    31
Bob    12    36
Kent   88    43
Kent   12    33
Alice  90    32
Ross   67    77
Alice  56    92
Jimmy  33    88
Jimmy  11    87
EOS

から出力

player  gameA gameB total
Alice   230   203   433
Jimmy   124   206   330
Kent    190   91    281
Ross    145   125   270
Bob     110   147   257

を得よという問題です(totalで降順)。

元ブログでの回答はこちら。

require "csv"

class CSV
  def group_by(&blk)
    Hash[ super.map { |k, v| [k, CSV::Table.new(v)] } ]
  end
end

csv = CSV.new(data, col_sep:' ', headers:true, converters: :numeric, header_converters: :symbol)
scores_by_player = csv.group_by(&:first)
stat = scores_by_player.map do |(_, player), t|
  ab = [:gamea, :gameb].map { |e| t[e].inject(:+) }
  [player, *ab, ab.inject(:+)]
end
puts "%s\t%s\t%s\ttotal" % csv.headers
puts stat.sort_by{ |s| -s.last }.map { |line| "%s\t%d\t%d\t%d" % line }

標準添付ライブラリを使っているわけですね。しかし、メソッドのオーバーライドはさすがにちょっとという気がします。それに、コードが凝りすぎて自分には読みにくい感じ。

ライブラリを使わず、極ふつうに素直にやったらどうなるか、考えてみました。

header, *given = data.each_line.map(&:split)
ga, gb = Hash.new(0), Hash.new(0)
given.each do |name, a, b|
  ga[name] += a.to_i
  gb[name] += b.to_i
end

table = [header + ["total"]] +
        given.map(&:first).uniq.map {|n| [n, ga[n], gb[n], ga[n] + gb[n]]}
        .sort {|a, b| b[3] <=> a[3]}
puts table.map {|p, a, b, t| sprintf "%s\t%s\t%s\t%s", p, a, b, t}

結構めんどうですね。もっとうまくできますかね。

線分の交点(Ruby)

4点 があるとき、線分 の交点を求めるメソッド。
 

require 'matrix'

def cross(x1, y1, x2, y2, x3, y3, x4, y4)
  a = Matrix[[x2 - x1, x3 - x4], [y2 - y1, y3 - y4]]
           .lup.solve([x3 - x1, y3 - y1]) rescue nil
  return nil unless a
  s, t = a[0], a[1]
  f = ((0 <= s and s <= 1) and (0 <= t and t <= 1))
  f ? Vector[x1 + s * (x2 - x1), y1 + s * (y2 - y1)] : nil
end

交点(端点でも OK)があれば Vector で返し、なければ nil を返します。

標準添付ライブラリを使わない場合。Vector の代わりに配列を返します。

def cross(x1, y1, x2, y2, x3, y3, x4, y4)
  l = (x2 - x1) * (y4 - y3) - (y2 - y1) * (x4 - x3)
  return nil if l.zero?
  vx, vy = x3 - x1, y3 - y1
  s = Rational((y4 - y3) * vx - (x4 - x3) * vy, l)
  t = Rational((y2 - y1) * vx - (x2 - x1) * vy, l)
  f = ((0 <= s and s <= 1) and (0 <= t and t <= 1))
  f ? [x1 + s * (x2 - x1), y1 + s * (y2 - y1)] : nil
end

 
線分でなくて直線の交点の場合は、メソッドでの s, t の範囲のチェックをおこなわなければよいです。

Ruby のローカル変数登録について

以下の Ruby コードを見てほしい。

if false
  a = 1
end
p a    #=>nil

if true
  nil
else
  b = 1
end
p b    #=>nil

c = 1 if false
p c    #=>nil

while false
  d = 1
end
p d    #=>nil

いずれも nil を出力し、「undefined local variable or method」の NameError は出ない。

覚えておくと役に立つかも知れない。

応用編。

$ pry
[1] pry(main)> i = 1 if i.nil?
=> 1
[2] pry(main)> i
=> 1
[3] pry(main)> j = 1 if j == 1
=> nil
[4] pry(main)> j
=> nil

これはたぶん何の役にも立たない。

二次元配列の行(あるいは列)を入れ替える(Ruby)

Julia では簡単とあったので(よく知らない)、Ruby で考えてみた。

まず二次元配列を作る。

$ pry
[1] pry(main)> ary = (1..100).each_slice(10).to_a
=> [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
 [21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
 [31, 32, 33, 34, 35, 36, 37, 38, 39, 40],
 [41, 42, 43, 44, 45, 46, 47, 48, 49, 50],
 [51, 52, 53, 54, 55, 56, 57, 58, 59, 60],
 [61, 62, 63, 64, 65, 66, 67, 68, 69, 70],
 [71, 72, 73, 74, 75, 76, 77, 78, 79, 80],
 [81, 82, 83, 84, 85, 86, 87, 88, 89, 90],
 [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]]

 
行を入れ替えるのは簡単。3行目と5行目を入れ替えてみる。

[2] pry(main)> ary[2], ary[4] = ary[4], ary[2]
[3] pry(main)> ary
=> [[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
 [11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
 [41, 42, 43, 44, 45, 46, 47, 48, 49, 50],
 [31, 32, 33, 34, 35, 36, 37, 38, 39, 40],
 [21, 22, 23, 24, 25, 26, 27, 28, 29, 30],
 [51, 52, 53, 54, 55, 56, 57, 58, 59, 60],
 [61, 62, 63, 64, 65, 66, 67, 68, 69, 70],
 [71, 72, 73, 74, 75, 76, 77, 78, 79, 80],
 [81, 82, 83, 84, 85, 86, 87, 88, 89, 90],
 [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]]

 
列を交換するのは少し工夫が要る。さらに3列目と5列目を入れ替えてみる。

[4] pry(main)> ary.each {|a| a[2], a[4] = a[4], a[2]}
[5] pry(main)> ary
=> [[1, 2, 5, 4, 3, 6, 7, 8, 9, 10],
 [11, 12, 15, 14, 13, 16, 17, 18, 19, 20],
 [41, 42, 45, 44, 43, 46, 47, 48, 49, 50],
 [31, 32, 35, 34, 33, 36, 37, 38, 39, 40],
 [21, 22, 25, 24, 23, 26, 27, 28, 29, 30],
 [51, 52, 55, 54, 53, 56, 57, 58, 59, 60],
 [61, 62, 65, 64, 63, 66, 67, 68, 69, 70],
 [71, 72, 75, 74, 73, 76, 77, 78, 79, 80],
 [81, 82, 85, 84, 83, 86, 87, 88, 89, 90],
 [91, 92, 95, 94, 93, 96, 97, 98, 99, 100]]

こんな感じでどうですかね。

ふう、Ruby でも一応できるかな。
 

非破壊的変更で

上の例では元の配列は破壊されてしまう。非破壊的にやるのは、もう少し手間である。それぞれ次のようにやればできる。

行の入れ替え。

exchanged = ary.dup
exchanged[2], exchanged[4] = exchanged[4], exchanged[2]

列の入れ替え。

exchanged = ary.map(&:dup)
exchanged.each {|a| a[2], a[4] = a[4], a[2]}

 
以上で、元の配列 ary は変更されない。 なお、dup を使わず Marshal.load(Marshal.dump(ary)) のように deep copy してもよいが、ここではそこまですることもない。

Ruby と rcairo でベジェ曲線を描いてみる

ベジェ曲線Wikipedia)は滑らかな曲線を描くために使われるものです。いくつかの「制御点」を指定して描きます。計算はそんなにむずかしくなくて、上の Wikipedia の記事で充分わかりますし、ネット上にわかりやすい記事がたくさんあるので検索してみて下さい。

Ruby と 'rcairo' で描いてみた例です。
20190320195050
青い線が制御点を結んだ折れ線で、赤い曲線がベジェ曲線です。

Ruby コード。
bezier_curve.rb

require 'cairo'
require 'matrix'

class BezierCurve
  def initialize(points, step = 0.01)
    @points = points.map {|a| a.class == Array ? Vector[*a] : a}
    @step = step
  end
  
  def calc
    n = @points.size - 1
    Enumerator.new do |y|
      c = [1] + (1..n).map {|k| (n - k + 1..n).inject(&:*) / (1..k).inject(&:*)}
      0.0.step(1.0, @step) do |t|
        j = ->(i) { c[i] * t ** i * (1 - t) ** (n - i) }
        y << @points.map.with_index {|b, i| b * j.(i)}.inject(&:+)
      end
      y << @points.last
    end
  end
end


if __FILE__ == $0
  #画像の大きさ
  W = 300
  
  #cairoの初期設定
  surface = Cairo::ImageSurface.new(W, W)
  context = Cairo::Context.new(surface)
  
  #背景
  context.set_source_color(Cairo::Color.parse("#F1F389"))
  context.rectangle(0, 0, W, W)
  context.fill
  
  #制御点を与える
  points = [[20.0, 280.0], [60.0, 100.0], [200.0, 120.0], [290.0, 230.0]]
  
  #制御点を結ぶ青い線
  context.set_source_color(Cairo::Color::BLUE)
  context.set_line_width(2)
  context.move_to(*points.first)
  points.drop(1).each {|r| context.line_to(*r)}
  context.stroke
  
  #ベジェ曲線(赤色)の描画
  context.set_source_color(Cairo::Color::RED)
  context.move_to(*points.first)
  
  BezierCurve.new(points).calc.each do |r|
    context.line_to(*r.to_a)
  end
  context.stroke
  
  #png画像として出力
  surface.write_to_png("bezier_curve.png")
end

使い方としては、配列 points に制御点を入れて(制御点は配列あるいは Vector クラスで表現します)、BezierCurve.new(points, step).calc で折れ線(step を細かくすればベジェ曲線に見えるわけです)の頂点(Vector クラスで表現されています)を順に与える Enumerator を返します。step は省略されれば 0.01 がデフォルトになります。曲線は t = 0 が開始で t = 1 が終了なので、step は一回に進む t の値を指定します。step = 0.01 ならば曲線が 100分割されるということです。


なお、なめらかな曲線としては「スプライン曲線」というのもあります。下の記事で扱っています。
obelisk.hatenablog.com

Gem 'Ruby2D' でライフゲーム

20190320020235
いつもの得意技(?)のライフゲームです。Ruby 用のグラフィック・ライブラリ 'Ruby2D' を使っています。

コード。
lifegame_for_Ruby2D.rb

require 'ruby2d'
include Ruby2D::DSL

class LifeGame
  CellWidth = 10
  Margin = 20
  Space = 2
  SideWidth = 35
  W = CellWidth * SideWidth + Space * (SideWidth - 1) + Margin * 2
  
  def initialize(num)
    set width: W, height: W, title: "LifeGame", fps_cap: 3
    
    cells = SideWidth.times.map do |y|
      SideWidth.times.map do |x|
        Square.new x: Margin + (CellWidth + Space) * x,
                   y: Margin + (CellWidth + Space) * y,
                   size: CellWidth,
                   color: Color.new([rand, rand, rand, 1.0]),
                   z: 0
      end
    end
    each_cell {|x, y| cells[y][x].remove}
    
    f = [1] * num + [0] * (SideWidth ** 2 - num)
    f.shuffle!
    @field = f.each_slice(SideWidth).to_a
    
    clear
    
    update do
      each_cell do |x, y|
        @field[y][x].nonzero? ? cells[y][x].add : cells[y][x].remove
      end
      next_field
    end
  end
  
  def each_cell
    SideWidth.times {|y| SideWidth.times {|x| yield(x, y)} }
  end
  
  def next_field
    tmp = Array.new(SideWidth) {Array.new(SideWidth, 0)}
    each_cell do |x, y|
      num = neighbor(x, y)
      if @field[y][x].nonzero?
        tmp[y][x] = 1 if num == 2 or num == 3
      else
        tmp[y][x] = 1 if num == 3
      end
    end
    @field = tmp
  end
  
  def neighbor(x, y)
    dirs = [[1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0], [-1, 1], [0, 1], [1, 1]]
    dirs.map do |dx, dy|
      x1 = x + dx
      y1 = y + dy
      next if x1 < 0 or x1 >= SideWidth
      next if y1 < 0 or y1 >= SideWidth
      @field[y1][x1]
    end.compact.inject(&:+)
  end
  
  def go
    show
  end
end

LifeGame.new(250).go

ちょっとチュートリアルだけではわからないところがあったので、ソースコードを見る必要がありました。まずクラスの中で Ruby2D のメソッドをどう使ったらよいかで、これは最初に include Ruby2D::DSL を宣言しておくことで解決(他の方法もあります)。それから、メソッド update のブロックの中で状態がその都度更新されるのですが、そのフレーム数の指定はどうすればよいか。これはメソッド set のキーワード引数で :fps_cap で指定してやればできることがわかりました。なお、ソースコードはとても読みやすいものでした。
 
'Ruby2D' については以下で紹介しています。
obelisk.hatenablog.com
 

上とは関係のないどうでもいいこと

Ruby は型の指定がないのでコードが読みにくいという根強い意見がありますが、たぶんこれではないかと。

  • そもそもそのコード自体が読みにくい、クソなものである → Ruby のコードは一般にシンプルで短めなので、さっと読むのは楽な方です
  • 全体のコードが長すぎて読みにくい → ソースファイルを分割する必要がある
  • Ruby にはジェネリクスがない → Ruby の書き方を知らないにもほどがある

なお、これらはじつは他の言語にも応用できることです。別に Ruby は完璧な言語ではありませんが、実用的な(といってもまあいろいろでしょうけれど)言語としては非常によくできたものです。オブジェクト指向言語としてはもっともよくできたもののひとつでしょうし、クロージャ、第一級関数もサポートしており、関数型プログラミングのエッセンスも詰まっています。初心者にもやさしいですし、高度なプログラミングのできるプログラマにはそれなりの優れた書き方ができます。あんまり知らないでバカにするのはやめましょう。

僕は Ruby 以外では Go が結構好きなので(ソースコードの見た目が好きです笑)、Ruby と Go が連携できるような仕組みを作って欲しいのですけれど…。C言語がうまく書けるようになるのはなかなか大変…。

それから、「デザインパターンはオワコン」という意見も見ましたが、オワコンであろうが知っておいた方がよいと思います。純粋にプログラミング技術としておもしろいものです。最近はプロのプログラマの絶対数が増えたので、プロであり自信満々でもじつはよくわかっていない人も多いですから、我々素人のプログラミング好きは気をつけたいものです。


しかし、PythonPerl で型の指定がないので読みにくいという意見は、ほとんど目にしたことがないのだが…。

Gem 'Ruby 2D' で遊ぶ(1)

20190308180919
 
Ruby でグラフィック表示のできる Gem 'Ruby 2D' で遊んでみました。アニメーションをしています。三角が丸たちの上にあります。色は半透明になっています。
コード。
ruby2d_sample2.rb

require 'ruby2d'

Width = 500
C = 15    #円の数
R = 20    #円の半径

L = 150   #三角形の一辺の長さ

set width: Width, height: Width

circles = C.times.map {
  Circle.new x: rand(R..Width - R), y: rand(R..Width - R),
     radius: R, color: Color.new([rand, rand, rand, 0.8]), z: 0
}
cvs = circles.map {[rand(-3.0..3.0), rand(-3.0..3.0)]}    #円の移動ベクトル

triangle = 4.times.map {|i|
  cn = Width / 2.0
  xs = [cn - L / 2.0, cn + L / 2.0]
  height = L * 0.866
  
  case i
  when 0, 1
    j = i * 2 - 1
    x1, y1 = xs[0], cn - height / 2 * j
    x2, y2 = xs[1], cn - height / 2 * j
    x3, y3 = cn, cn + height / 2 * j
  else
    j = i * 2 - 5
    x1, y1 = cn - height / 2 * j, xs[0]
    x2, y2 = cn - height / 2 * j, xs[1]
    x3, y3 = cn + height / 2 * j, cn
  end
  
  Triangle.new x1: x1, y1: y1, x2: x2, y2: y2, x3: x3, y3: y3,
     color: "#FAB536", z: 10, opacity: 0.8
}
triangle.each(&:remove)

h = {0=>0, 1=>2, 2=>1, 3=>3}
t = 0

update do
  #円の移動
  circles.zip(cvs).each do |c, vec|
    c.x += vec[0]
    c.y += vec[1]
    c.x = Width + R if c.x < -R
    c.y = Width + R if c.y < -R
    c.x = -R if c.x > Width + R 
    c.y = -R if c.y > Width + R 
  end
  
  #三角形の回転
  k = t / 100 % 4
  triangle[h[k]].add
  triangle[h[(k - 1) % 4]].remove
  
  t += 1
end

show

 
'Ruby 2D' についてはこちらで紹介しています。
obelisk.hatenablog.com