最大値をもつものを集める(Ruby)

例えば都道府県名をローマ字化したものから、文字数の最大値と、その文字数をもつすべての県名を得たいとする。そのとき、こんなメソッドを作ってみるといいかも知れない。
 

module ExEnumerable
  refine Enumerable do
    def max_select
      pool = []
      max_num = -Float::INFINITY
      each do |i|
        n = yield(i)
        if n > max_ num
          max_ num = n
          pool = [i]
        elsif n == max_ num
          pool << i
        end
      end
      [max_ num, pool]
    end
  end

  [Array, Enumerator, Hash].each {|mdl| mdl.include Enumerable}
end

Enumerable#max_selectは、ブロックの返り値の最大値(max_num)を求めて、そのような最大値になるようなものをレシーバーから集め(pool)、[max_num, pool]を返す。

これを使って、最初の課題を解いてみる。

require "open-uri"
using ExEnumerable

url = "https://gist.githubusercontent.com/koseki/38926/raw/671d5279db1e5cb2c137465e22424c6ba27f4524/todouhuken.txt"
prefectures = URI.open(url).each_line.map {|l| l.chomp.split.last}
prefectures.max_select(&:size)
#=>[9, ["fukushima", "yamanashi", "hiroshima", "yamaguchi", "tokushima", "kagoshima"]]

9文字が最大値だとわかる。県名のリストも返ってくる。


※参考
Ruby で関数型プログラミングっぽく(コピペ) + Haskell 版 - Camera Obscura

Enumerator::Lazy でエラトステネスの篩(Ruby)

Enumerator::Lazy は無限数列を扱うことができるが、それを使って「エラトステネスの篩」をちょっとおもしろく実装できることに気づいた。何はともあれコードである。

lazy_prime.rb

prime_seq = Enumerator.new do |y|
  sieve = 2.step.lazy
  loop do
    a = sieve.first
    y << a
    sieve = sieve.reject {|x| (x % a).zero?}
  end
end

prime_seq.take(10)    #=>[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]

 
Enumerator::Lazy はsieve = 2.step.lazyのところで生成している。これの先頭aは必ず素数なので、素数列として Enumerator で取り出す(y << a)。そして、sieveからaの倍数をrejectですべて篩い落とし(Lazy だから可能なのである)、そうされたものを新しいsieveとする。以下、繰り返し、というわけである。

これは子供の頃に紙と鉛筆でやってみた「エラトステネスの篩」のやり方そのものである。小さい方から素数に丸でも打っておいて、その倍数を斜線で消していく…。実装としては、素直といえば素直だ。

しかし、これ、めちゃめちゃに遅いのはすぐわかる。素数ひとつ得るたびに、Enumerator::Lazy オブジェクトを作り直しているのだから、どれほど遅いかというと、例えば最初の 500個の素数を得るのに、この方法だとわたしの環境でなんと 2.8秒ほどもかかる。標準添付ライブラリの prime を使うと 0.0002秒ほどだから、比較にもならない。

でもまあ、ちょっとおもしろかったので書いてみた。


なお、単なる Enumerator ももちろん無限数列を扱うことができるが、mapselect、それにここで使ったrejectなどの結果が有限数列(Array)になってしまう。なので、Enumerator::Lazy を使ったのである。為念。

MP3ファイルの分割(ffmpeg, Ruby)

ffmpegRuby を使って、MP3ファイルを分割します。

こんな感じ。
cut_mp3.rb

file = DATA.gets.chomp
ts = DATA.gets.split.map {|t| t.split(":").map(&:to_i)}

Dir.chdir(File.dirname(file))
bname = File.basename(file, ".mp3")
ts.map {_1 * 3600 + _2 * 60 + _3}.each_cons(2).with_index(1) do |(s, e), i|
  `ffmpeg -i "#{file}" -ss #{s} -t #{e - s} "#{bname + ("_%02d" % i)}.mp3"`
end


__END__
/home/***/Music/TAPEMP3/Bach.mp3
00:00:00 00:18:20 00:36:57 00:56:20 01:14:23 01:38:45 01:58:20

これだと、パス /home/***/Music/TAPEMP3/Bach.mp3 を6ファイルに分割します。最初のファイルは 00:00:00~00:18:20 まで、2番目のファイルは 00:18:20~00:36:57 までで、最後のファイルは 01:38:45~01:58:20 までとなります。

ruby 3.0.0preview1 をちょっと使ってみる

環境は Linux Mint 20 です。
 

Ractor

まずは Ractor。
https://github.com/ruby/ruby/blob/master/doc/ractor.md
https://github.com/ko1/ruby/blob/ractor/ractor.ja.md
下を見ていじくるとよい。
qiita.com
 
とりあえずこれ。

Ractor.new do
  5.times do
    puts :hello
  end
end

5.times do
  puts :world
end

結果。こうなってしまう。

world
world
world
world
world

これは Ractor の生成にコストがかかり、「hello」が出力される前にインタプリタが終了してしまうため。
なので、こうしてみる。

Ractor.new do
  5.times do
    puts :hello
    sleep(rand(0.1))
  end
end

5.times do
  puts :world
  sleep(rand(0.1))
end

結果。

world
hello
hello
hello
world
hello
world
hello
world
world

うまくいきました。
 

Thread との比較

Thread。

N = 10
start = Time.now

th = N.times.map {
  Thread.new do
    a = 0
    1_000_000.times {a += 1}
    Time.now
  end
}.map(&:value)

puts th.map {_1 - start}.max    #=>0.576966553

Ractor。

N = 10
start = Time.now
rs = []

rs = N.times.map {
  Ractor.new do
    a = 0
    1_000_000.times {a += 1}
    Time.now
  end
}.map(&:take)

puts rs.map {_1 - start}.max    #=>0.365015356

4コア環境で、確かに高速化されている。
 

終わった順にすべての Ractor を待つ

例えばこんなメソッドを作る。

def Ractor.wait_for(*ractors)
  ractors.size.times.map do
    r, obj = Ractor.select(*ractors)
    ractors.delete(r)
    obj
  end
end

で、こんな感じ。

N = 10
start = Time.now

rs = N.times.map do
  Ractor.new do
    a = 0
    1_000_000.times {a += 1}
    Time.now
  end
end

result = Ractor.wait_for(*rs)
puts result.map {_1 - start}

結果。

0.191312097
0.231078089
0.284552986
0.292159854
0.323803118
0.339945751
0.349074802
0.35051055
0.352556214
0.363963214

カーリルAPIで遊んでみる(Ruby)

20200722222030
「カーリル」は全国の図書館の蔵書検索サイトです。APIが用意されているので、Ruby でちょっと遊んでみました。

まずはサイトから「アプリケーションキー」を取得して下さい。以下の appkey には、実際に取得したキーが入ります。
 

図書館の検索

わたしの住んでいる岐阜県各務原市の図書館情報を取得してみます。
コード。

require "net/http"

appkey = "MyApplicationKey"

uri = URI.parse("https://api.calil.jp/library")
q = {appkey: appkey, pref: "岐阜県", city: "各務原市"}
uri.query = URI.encode_www_form(q)

response = Net::HTTP.get_response(uri)
puts response.code
puts response.body

Net::HTTP.get_response でレスポンスを取得します。結果。

200
<?xml version="1.0" encoding="utf-8"?>
<Libraries>
  <Library>
    <systemid>Gifu_Kakamigahara</systemid>
    <systemname>岐阜県各務原市</systemname>
    <libkey>もりの本やさん</libkey>
    <libid>100889</libid>
    <short>もりの本やさん</short>
    <formal>各務原市もりの本やさん</formal>
    <url_pc>http://ufinity08.jp.fujitsu.com/kakamigahara/</url_pc>
    <address>岐阜県各務原市鵜沼字石山6529-2</address>
    <pref>岐阜県</pref>
    <city>各務原市</city>
    <post>509-0111</post>
    <tel>058-370-7175</tel>
    <geocode>136.950857,35.410975</geocode>
    <category>SMALL</category>
    <image/>
  </Library>
  <Library>
    <systemid>Gifu_Kakamigahara</systemid>
    <systemname>岐阜県各務原市</systemname>
    <libkey>中央ライフ</libkey>
    <libid>100890</libid>
    <short>中央ライフデザインセンター</short>
    <formal>各務原市中央ライフデザインセンター図書室</formal>
    <url_pc>http://ufinity08.jp.fujitsu.com/kakamigahara/</url_pc>
    <address>岐阜県各務原市蘇原中央町2-1-8 中央ライフデザインセンター2階</address>
    <pref>岐阜県</pref>
    <city>各務原市</city>
    <post>504-0813</post>
    <tel>058-389-1820</tel>
    <geocode>136.8791775,35.410405</geocode>
    <category>SMALL</category>
    <image/>
  </Library>
  <Library>
    <systemid>Gifu_Kakamigahara</systemid>
    <systemname>岐阜県各務原市</systemname>
    <libkey>中央図書館</libkey>
    <libid>100892</libid>
    <short>中央図書館</short>
    <formal>各務原市立中央図書館</formal>
    <url_pc>http://ufinity08.jp.fujitsu.com/kakamigahara/</url_pc>
    <address>岐阜県各務原市那加門前町3丁目1-3</address>
    <pref>岐阜県</pref>
    <city>各務原市</city>
    <post>504-0911</post>
    <tel>058-383-1122</tel>
    <geocode>136.842595,35.400056</geocode>
    <category>MEDIUM</category>
    <image/>
  </Library>
  <Library>
    <systemid>Gifu_Kakamigahara</systemid>
    <systemname>岐阜県各務原市</systemname>
    <libkey>川島ほんの家</libkey>
    <libid>100891</libid>
    <short>川島ほんの家</short>
    <formal>各務原市川島ほんの家</formal>
    <url_pc>http://ufinity08.jp.fujitsu.com/kakamigahara/</url_pc>
    <address>岐阜県各務原市川島松倉町1951-4 川島会館3階</address>
    <pref>岐阜県</pref>
    <city>各務原市</city>
    <post>501-6022</post>
    <tel>0586-89-5610</tel>
    <geocode>136.8254624,35.3679736</geocode>
    <category>SMALL</category>
    <image/>
  </Library>
  <Library>
    <systemid>Gifu_Kakamigahara</systemid>
    <systemname>岐阜県各務原市</systemname>
    <libkey>移動図書館</libkey>
    <libid>100888</libid>
    <short>さつき号</short>
    <formal>各務原市移動図書館「さつき号」</formal>
    <url_pc>http://ufinity08.jp.fujitsu.com/kakamigahara/</url_pc>
    <address>岐阜県各務原市那加門前町3丁目1-3</address>
    <pref>岐阜県</pref>
    <city>各務原市</city>
    <post>504-0911</post>
    <tel>058-383-1122</tel>
    <geocode>136.8425677,35.4000383</geocode>
    <category>BM</category>
    <image/>
  </Library>
  <Library>
    <systemid>Univ_Chubu_Gu</systemid>
    <systemname>中部学院大学</systemname>
    <libkey>各務原</libkey>
    <libid>104938</libid>
    <short>各務原図書館</short>
    <formal>中部学院大学附属図書館各務原キャンパス図書館</formal>
    <url_pc>http://web3.chubu-gu.ac.jp/library/</url_pc>
    <address>岐阜県各務原市那加甥田町30-1</address>
    <pref>岐阜県</pref>
    <city>各務原市</city>
    <post>504-0837</post>
    <tel>058-375-3607</tel>
    <geocode>136.845140,35.404969</geocode>
    <category>UNIV</category>
    <image/>
  </Library>
  <Library>
    <systemid>Univ_Tokaigakuin</systemid>
    <systemname>東海学院大学</systemname>
    <libkey>本館</libkey>
    <libid>104710</libid>
    <short>附属図書館</short>
    <formal>東海学院大学・東海学院大学短期大学部附属図書館</formal>
    <url_pc>http://www.tokaigakuin-u.ac.jp/library/</url_pc>
    <address>岐阜県各務原市那加桐野町5丁目68</address>
    <pref>岐阜県</pref>
    <city>各務原市</city>
    <post>504-8511</post>
    <tel>058-389-2969</tel>
    <geocode>136.818267,35.424550</geocode>
    <category>UNIV</category>
    <image/>
  </Library>
</Libraries>

ステータスコードは 200 なので、GET は成功しています。レスポンスの中身はデフォルトで xml の形式になっています。
わたしは中央図書館の情報が知りたいので、systemid は「Gifu_Kakamigahara」だということがわかりました。下でこれを使います。
 

蔵書検索

各務原市中央図書館に、下の本が架蔵されているか調べます。たまたまいまわたしが借りている本です(笑)。

八九六四 「天安門事件」は再び起きるか

八九六四 「天安門事件」は再び起きるか

systemid と isbnコードを使ってリクエストします。

require "net/http"
require "json"

appkey = "MyApplicationKey"

uri = URI.parse("https://api.calil.jp/check")
q = {appkey: appkey, isbn: "9784041067352", systemid: "Gifu_Kakamigahara", callback: :no}
uri.query = URI.encode_www_form(q)

response = Net::HTTP.get_response(uri)
hash = JSON.parse(response.body)

puts response.code
pp hash

結果。

200
{"session"=>"c5584394d201695694d0e0ef13df6119",
 "continue"=>0,
 "books"=>
  {"9784041067352"=>
    {"Gifu_Kakamigahara"=>
      {"status"=>"Cache",
       "libkey"=>{"中央図書館"=>"貸出中"},
       "reserveurl"=>
        "https://ilisod001.apsel.jp/kakamigahara-library/wopc/pc/OpacServlet?disp=searchResultDetail&id=2728033"}}}}

"continue" の結果が 0 なので、問い合わせが上手くいったようです。中央図書館に蔵書されていて、確かに貸出中ですね(笑)。

結果が次のような場合になることもあります。

{"session"=>"2c476142999d400a13b2ce1020501f58",
 "continue"=>1,
 "books"=>
  {"9784041067352"=>
    {"Gifu_Kakamigahara"=>{"status"=>"Running", "reserveurl"=>""}}}}

"continue" の結果が 1 なので、継続した問い合わせが必要です。


継続した問い合わせを考慮に入れたコード。また、問い合わせる本も図書館も複数(岐阜県図書館も探す)にしてみました。
calil.rb

require "net/http"
require "json"

appkey = "MyApplicationKey"

uri = URI.parse("https://api.calil.jp/check")
q = {appkey: appkey, callback: :no}
q.merge!({isbn: "9784041067352,9784393142714"})
q.merge!({systemid: "Gifu_Kakamigahara,Gifu_Pref"})
uri.query = URI.encode_www_form(q)

response = hash = nil

loop do
  response = Net::HTTP.get_response(uri)
  hash = JSON.parse(response.body)
  break if hash["continue"].zero?
  puts "continue"
  sleep(3)
  q = {appkey: appkey, session: hash["session"], callback: :no}
  uri.query = URI.encode_www_form(q)
end

puts response.code
pp hash

結果。

continue
200
{"session"=>"7d3d755be59c6f7c6364198d9408773f",
 "continue"=>0,
 "books"=>
  {"9784041067352"=>
    {"Gifu_Pref"=>
      {"status"=>"Cache",
       "libkey"=>{"図書館"=>"貸出可"},
       "reserveurl"=>
        "https://www.library.pref.gifu.lg.jp/winj/opac/switch-detail-iccap.do?bibid=1190315471"},
     "Gifu_Kakamigahara"=>
      {"status"=>"OK",
       "libkey"=>{"中央図書館"=>"貸出中"},
       "reserveurl"=>
        "https://ilisod001.apsel.jp/kakamigahara-library/wopc/pc/OpacServlet?disp=searchResultDetail&id=2728033"}},
   "9784393142714"=>
    {"Gifu_Pref"=>
      {"status"=>"Cache",
       "libkey"=>{"図書館"=>"貸出中"},
       "reserveurl"=>
        "https://www.library.pref.gifu.lg.jp/winj/opac/switch-detail-iccap.do?bibid=1100029995"},
     "Gifu_Kakamigahara"=>
      {"status"=>"Cache", "libkey"=>{}, "reserveurl"=>""}}}}

mod p の世界における割り算とは(Ruby)

qiita.com
mod p における「逆元」を考えるとよいそう。

Ruby で実装する(元は C++)。

def modinv(a, m)
  b, u, v = m, 1, 0
  until b.zero?
    t = a / b
    a -= t * b
    a, b = b, a
    u -= t * v
    u, v = v, u
  end
  u %= m
  u += m if u < 0
  u
end

自然数の約数の個数(Ruby)

自然数の約数の個数は、素因数分解をして求める。Ruby なら、prime ライブラリを require して、Inetger#prime_divisionメソッドを使う。

require "prime"

n = 360

puts n.prime_division.inject(1) {|acc, (a, b)| acc * (b + 1)}    #=>24