カーリル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"=>""}}}}

Ruby2D で穴掘り迷路


見てのとおり、穴掘り迷路です。Gem 'ruby2d' を使いました。Ruby2D についてはこちら

コード。
dig_maze.rb

require "ruby2d"

L = 20    #迷路の大きさ
W = L * 2 + 3

Block_w = 10
set width: W * Block_w, height: W * Block_w, fps_cap: 10

blocks = W.times.map {|y|
  W.times.map {|x|
    Square.new x: x * Block_w, y: y * Block_w,
               size: Block_w, color: "green"
  }
}

field = Array.new(W) {Array.new(W, 1)}
field[0] = field[-1] = Array.new(W, -1)
(1..W - 2).each {|y| field[y][0] = field[y][-1] = -1}

field.define_singleton_method(:show) do
  each_index do |y|
    self[y].each_index do |x|
      self[y][x].zero? ? blocks[y][x].remove : blocks[y][x].add
    end
  end
end

start = [2, 2]
stack = [start]
show_stack = [start]

dig = ->(now) {
  movable = []
  [[1, 0], [0, -1], [-1, 0], [0, 1]].each do |dx, dy|
    x = now[0] + dx * 2
    y = now[1] + dy * 2
    movable << [x, y] if field[y][x] == 1
  end
  if movable.empty?
    return if stack.empty?
    jump = stack.delete_at(rand(stack.size))
    dig.(jump)
  else
    nxt = movable.sample
    show_stack << [(now[0] + nxt[0]) / 2, (now[1] + nxt[1]) / 2]
    show_stack << nxt
    stack << nxt
  end
}


update do
  now = show_stack.shift
  next unless now
  field[now[1]][now[0]] = 0
  field.show
  dig.(now) if show_stack.empty?
end

show

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

過去記事のプログラムを少し改変したものです。
 
コード。
ruby2d_sample2a.rb

require 'ruby2d'
require 'matrix'
include Math

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

L = 70    #三角形の外接円の半径

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)]}    #円の移動ベクトル
circles_move = circles.zip(cvs)


rot = ->(θ) {
   θ1 = θ / 180.0 * PI
   Matrix[[cos(θ1), sin(θ1)], [-sin(θ1), cos(θ1)]]
}

points = ->(θ) {
  o = Vector[Width / 2.0, Width / 2.0]
  v0 = Vector[0, -L]
  p1 = o + rot.(θ +   0) * v0
  p2 = o + rot.(θ + 120) * v0
  p3 = o + rot.(θ + 240) * v0
  [p1, p2, p3]
}

triangle = Triangle.new z: 10, opacity: 0.8

triangle.define_singleton_method(:rotate) do |θ|
  p1, p2, p3 = points.(θ)
  self.x1 = p1[0]
  self.y1 = p1[1]
  self.x2 = p2[0]
  self.y2 = p2[1]
  self.x3 = p3[0]
  self.y3 = p3[1]
end

triangle_color = [rand, rand, rand]
triangle_color_step =
   (Vector[rand(-1.0..1.0), rand(-1.0..1.0), rand(-1.0..1.0)]
     .normalize / 100).to_a

triangle_color.define_singleton_method(:update) do
  triangle.color = self + [0.8]
  replace(zip(triangle_color_step).map {|a, b| a + b})
  triangle_color.each_index do |i|
    if triangle_color[i] < 0 || triangle_color[i] > 1
      triangle_color_step[i] = -triangle_color_step[i]
    end
  end
end


θ = 0

update do
  #円の移動
  circles_move.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
  
  #三角形の回転
  triangle.rotate(θ)
  triangle_color.update if (θ % 3).zero?
  θ = (θ + 1) % 360
end

show

inject が Enumerator を返さない(Ruby)

例えば true/false の配列をビット列に変換したいとして、こうしたかった。

cond = [true, false, false, true, false, true]

cond.inject(0).with_index {|(acc, c), i| c ? acc | 1 << i : acc}
#=>TypeError (0 is not a symbol nor a string)

injectはブロックを取らない場合、Enumerator を返さないのですね。


仕方がないのでenum_forを使う。

cond.enum_for(:inject, 0).with_index {|(acc, c), i| c ? acc | 1 << i : acc}
#=>0b101001

これでinjectも Enumerator を返します。


しかし、enum_forってどうなっているのだろうね。どうも、ブロックを取るメソッドはすべて使えるらしい。変な例。

class String
  def upcase🍓
    yield(upcase)
  end
end

enum = "aaa".enum_for(:upcase🍓)
enum.map {|e| e + "0"}    #=>["AAA0"]

謎である。

Ruby で同値(必要十分)関係

Ruby で「p と q が同値」あるいは「必要十分」、つまり
 
の関係を表すにはどうしたらよいでしょうか。

これはp == qのことでも、p.equal?(q) のことでもありません。ではなくて、

p が T ならば q も T、p が F ならば q も F

ということです。T とか F は何だということになりますが、Rubytruefalseはちょっと曖昧で、Ruby の if ではnilfalseが「偽」と判定されますね。この「偽」になるオブジェクトを F、それ以外のオブジェクトを T とする、という意味です。真偽表だと

p q p⇔q
T T T
T F F
F T F
F F T

となります。

じつは同値 EQ は、排他的論理和 XOR の否定です。なので簡単そうですが、じつは Ruby には XOR を表す演算子がありません。
ここにあるように、Ruby で XOR を表すには、!!p ^ !!qあるいは!p ^ !qとすればよいようです。なので同値を判定するメソッドは

def eq(p, q)
  !(!p ^ !q)
end

とすればよいことになります。