ピッケル本を読む(4)第5章 標準型

Rubyにおける数値, 文字列, 範囲, 正規表現を詳しく説明してくれる章らしい。

メモ

  • 整数
  • 整数の最大値はメモリの空き容量によって決まる
  • 一定範囲の整数は、Fixnumクラスのオブジェクトとしてバイナリ形式で内部的に保持される
    • 一定範囲は通常、-2^30〜2^30-1、-2^62〜2^62-1の範囲らしい
  • Fixnumクラスの範囲外の整数は、Bignumクラスのオブジェクトに格納される
  • FixnumとBignumの間の変換は、Rubyが自動的に行う
num = 81
6.times do
  puts "#{num.class}: #{num}"
  num *= num
end
# 実行結果
Fixnum: 81
Fixnum: 6561
Fixnum: 43046721
Bignum: 1853020188851841
Bignum: 3433683820292512484657849089281
Bignum: 11790184577738583171520872861412518665678211592275841109096961
  • 基数の接頭辞
    • 2進数・・・0b
    • 8進数・・・0
    • 10進数・・・0d(デフォルト)
    • 16進数・・・0x
  • 数字列中のアンダースコアは無視される
p 123_456    #=>    123456

明日があるので今日はここまで。
再開。(2007年8月7日(火))

  • コントロール文字の整数値
書き方 整数値 意味
?\C-x 24 コントロールx
?\M-x 248 メタ文字x
?a 97 ASCII文字
?\n 10 改行コード
?\C-? 127 削除文字
  • Perlと違い、Rubyは数字だけを含む文字列を式内で使っても、自動的に数値には変換されない
Rubyでは意図通りに足し算されない
# 入力ファイルの内容
# 3 4
# 5 6
# 7 8

# 入力値が文字列として読み込まれる
File.open("some_file") do |some_file|
  some_file.each do |line|
    next if /^\s*$/ =~ line  # 空白行
    next if /^#/ =~ line     # シャープで始まる行
    v1, v2 = line.split      # 空白で行を分割
    print v1 + v2, " "
  end
end
出力結果
34 56 78
Integerメソッドを使えば意図通りに足し算される
# 入力値が文字列として読み込まれる
File.open("some_file") do |some_file|
  some_file.each do |line|
    next if /^\s*$/ =~ line  # 空白行
    next if /^#/ =~ line     # シャープで始まる行
    v1, v2 = line.split      # 空白で行を分割
    print Integer(v1) + Integer(v2), " "
  end
end
出力結果
7 11 15
  • 文字列リテラルを作成する方法は3つある
    • %q
    • %Q
    • ヒアドキュメント
# 区切り文字は何でも良いらしい
# シングルクオートと同じ?
%q/Big Fat Cat#{1+1}/    #=>    "Big Fat Cat#{1+1}"

# ダブルクオートと同じ?
%Q/Big Fat Cat#{1+1}/    #=>    "Big Fat Cat2"

# ヒアドキュメント
string = <<終端文字列
  hoge
  hoge
  hoge
終端文字列

# 通常、終端文字列は行頭から始まらなければならないが、
# <<-を使えば終端文字列をインデントできる
string = <<-終端文字列1, <<-終端文字列2
  Concat
終端文字列1
    enate
終端文字列2

出力結果
  Concat
    enate
  • 文字列の操作
    • p53の例でSongオブジェクトをputsの引数として渡しているけど、何で自動でto_sメソッドの戻り値(String型)が得られるんだろう?
File.open("songdata") do |song_file|
  songs = SongList.new

  song_file.each do |line|
    file, length, name, title = line.chomp.split(/\s*\|\s*/)
    songs.append(Song.new(title, name, length))
  end
  puts songs[1]
end
出力結果
Song: Wonderful World--Louis       Armstrong (2:58)

# SongListクラスの部分的な再掲(すぐ上の例で何やってるか分からなくなりそうなので、再び載せた)
class SongList
  def initialize
    @songs = Array.new
    @index = WordIndex.new
  end

  def append(song)
    @songs.push(song)
    self
  end

  def [](index)
    @songs[index]
  end
end
songdataのフォーマット
/jazz/j00132.mp3  |  3:45  |  Fats        Waler        | Ain't Misbehavin'
/jazz/j00319.mp3  |  2:58  |  Louis       Armstrong    | Wonderful World
/bgrass/bg0732.mp3|  4:09  |  Strength in Numbers      | Texas Red


上の文字列操作の例で、本当にto_sが呼び出されているのか気になったので、サンプルを作ってみた。

class Hoge
  def initialize()
  end

  def to_s
    "In to_s."
  end

  def piyo
    "In piyo."
  end
end

# うーん、確かにto_sが呼び出されるみたいだ
# 良く分からないけど、確認できたから良しとしよう
hoge = Hoge.new
puts hoge         #=>    In to_s
puts hoge.to_s    #=>    In to_s.
p hoge            #=>    In to_s.
p hoge.to_s       #=>    "In to_s."

よく分からなくて悩んでいたら、Ruby Reference Manual - るりまに解説が載っていた。putsの仕様で、引数にオブジェクトが与えられた場合は、そのオブジェクトのto_sが呼び出されるらしい。以下引用。

puts([obj[, obj2[, ....]]] )

obj と改行を順番に $> に出力します。引数がなければ改行のみを出力します。

引数が配列の場合、その要素と改行を順に出力します。配列や文字列以外のオブジェクトが引数として与えられた場合には、当該オブジェクトを最初に to_ary により配列へ、次に to_s メソッドにより文字列へ変換を試みます。ただし、nil に対しては文字列 "nil" を出力します

末尾が改行で終っている引数に対しては puts 自身は改行を出力しません。

説明してから使ってくださいよ…。調べながら読み進めろって事なのかな。とりあえず、今日はここまで。
再開。(2007年8月10日(金))

  • 曲の時間を秒単位に変換して表示する
File.open("songdata") do |song_file|
  songs = SongList.new

  song_file.each do |line|
    file, length, name, title = line.chomp.split(/\s*\|\s*/)
    name.squeeze!(" ")
    # 正規表現に基づいて文字列をチャンクに分割(1桁以上の数字にマッチ)
    # 区切り文字ではなく、チャンクにマッチさせる点がsplitと違う
    mins, secs = length.scan(/\d+/)
    songs.append(Song.new(title, name, mins.to_i*60+secs.to_i))
    songs.append(Song.new(title, name, length))
  end
  puts songs[1]
end
出力結果
Song: Wonderful World--Louis Armstrong (178)


ほとんどやってないけど、今日は眠いのでここまで。
再開。(2007年8月13日(月))

  • ジュークボックスは検索機能も備えてる
    • WordIndexクラスは、他のクラスから使われるだけのユーティリティ用のクラスなのかな?
    • アクセス制御が実装に全く出てこないけど、その辺は無視なのかな?
class WordIndex
  def initialize
    # 空のハッシュを作る
    @index = {}
  end

  # phrasesの前の「*」は、可変長引数という意味。phraseという配列が作られる。
  def add_to_index(obj, *phrases)
    phrases.each do |phrase|
      phrase.scan(/\w[-\w']+/) do |word|
# wordに何が登録されているか確かめておく(DEBUG用)
p word
        # 大文字小文字を区別しないので、全て小文字に変換
        word.downcase!
        # 次の2行は、「@index[word] = obj」で置き換えられそうだけど、何か違いがあるのかな?
        @index[word] = [] if @index[word].nil?
        @index[word].push(obj)
      end
    end
  end

  def lookup(word)
    @index[word.downcase]
  end
end
  • 曲が追加され次第、索引付けしたいので、SongListクラスに、与えられた単語で曲を検索するメソッドを追加
class SngList
  def initialize
    @songs = Array.new
    @index = WordIndex.new
  end

  def append(song)
    @songs.push(song)
    # 曲が追加されたと同時に、索引付けしておく
    @index.add_to_index(song, song.name, song.artist)
    self
  end

  # 与えられた単語で曲を検索(検索の実装はWordIndexにある)
  def lookup(word)
    @index.lookup(word)
  end
end
  • 検索機能のテスト
    • 出力結果に余計な出力があるのは、WordIndexクラスでwordに何が入ってるか出力して確かめているため
File.open("songdata") do |song_file|
  songs = SongList.new

  song_file.each do |line|
    file, length, name, title = line.chomp.split(/\s*\|\s*/)
    name.squeeze!(" ")
    # 正規表現に基づいて文字列をチャンクに分割
    # 区切り文字ではなく、チャンクにマッチさせる点がsplitと違う
    mins, secs = length.scan(/\d+/)
    # SongListに登録されると同時に、WordIndexにも登録される。(SongList.append参照)
    songs.append(Song.new(title, name, mins.to_i*60+secs.to_i))
  end
  puts songs.lookup("Fats")
  puts songs.lookup("ain't")
  puts songs.lookup("RED")
  puts songs.lookup("WoRld")
end
出力結果
"Ain't"
"Misbehavin'"
"Fats"
"Waler"
"Wonderful"
"World"
"Louis"
"Armstrong"
"Texas"
"Red"
"Strength"
"in"
"Numbers"
Song: Ain't Misbehavin'--Fats Waler (225)
Song: Ain't Misbehavin'--Fats Waler (225)
Song: Texas Red--Strength in Numbers (249)
Song: Wonderful World--Louis Armstrong (178)


今日はここまで。
再開(2007年8月15日(水))

  • 範囲には以下の3つの異なる機能がある
    • シーケンスとしての範囲
    • 条件としての範囲
    • 間隔としての範囲
  • シーケンスとしての範囲
    • シーケンスは、開始点と終了点を持つ、一連の連続する値を生成する方法の一つ
    • Rubyでは".."と"..."範囲演算子を使用してシーケンスを作成する
# to_aメソッドを使って、範囲をリストに変換できる
p (1..10).to_a      #=>    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
('bar'..'bat').to_a #=>    ["bar", "bas", "bat"]

# ".."は両端を含み、"..."は終端を含まない
p (1..10).to_a      #=>    [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
p (1...10).to_a     #=>    [1, 2, 3, 4, 5, 6, 7, 8, 9]

# 1..10というシーケンスは、2つのFixnumオブジェクトへのリファレンスを含むRangeオブジェクトになる
p (1..10).class     #=>    Range
    • ユーザが自分で定義したオブジェクトをベースに範囲を作成することも出来る。ただし、次の2つの制約がある。
    • オブジェクトはsuccへの応答として、シーケンス内の「次の」オブジェクトを返さなければならない
    • オブジェクトは汎用の比較演算子<==>を使って比較できなければならない
class VU
  include Comparable

  attr :volume

  def initialize(volume)  # 0..9
    @volume = volume
  end

  def inspect
    '#' * @volume
  end

  # オブジェクトは汎用の比較演算子<==>を使って比較できなければならない
  # Support for ranges
  def <=>(other)
    self.volume <=> other.volume
  end

  # オブジェクトはsuccへの応答として、シーケンス内の「次の」オブジェクトを返さなければならない
  def succ
    raise(IndexError, "Volume too big") if @volume >= 9
    VU.new(@volume.succ)
  end
end

medium_volume = VU.new(4)..VU.new(7)
p medium_volume.to_a
p medium_volume.include?(VU.new(3))
実行結果
[####, #####, ######, #######]
false
  • 条件としての範囲
    • 条件式としての範囲はトグルスイッチのように動作する
    • 範囲の最初の条件が真:スイッチオン
    • 2番目の条件が真   :スイッチオフ
hani.rb
# 標準入力から読み込んで
while line = gets
  # startを含む業を見つけると出力開始し、endを含む行を見つけると出力終了する
  puts line if line =~ /start/ .. line =~ /end/
end

実行例
$ ruby -w hani.rb < sample.txt
start
1
2
3
end
 start
7
8
9
end

# sample.txtの内容
start
1
2
3
end
4
5
6
 start
7
8
9
end
10
  • 間隔としての範囲
    • これは、ある値が範囲の表す間隔に含まれているかどうかをチェックするもの
    • 条件等号演算子===を使う
$ irb
>> (1..10) === 5
=> true
>> (1..10) === 10
=> true
>> (1..10) === 11
=> false
>> (1..10) === 3.14
=> true
>> ('a'..'j') === 'c'
=> true
>> ('a'..'j') === 'k'
=> false


今日はここまで。
再開。(2007年8月20日(月))

# コンストラクタを明示的に呼び出す
a = Regexp.new('^\s*[a-z]')    #=>    /^\s*[a-z]/

# リテラル形式(/パターン/)
b = /^\s*[a-z]/                #=>    /^\s*[a-z]/

# リテラル形式(%r/パターン/)
c = %r{^\s*[a-z]}              #=>    /^\s*[a-z]/
  • マッチ演算子
  • マッチ演算子は、String オブジェクトと Regexp オブジェクトの両方に定義されている
  • マッチ演算子の少なくとも一方のオペランド正規表現でなければならない
    • Ruby1.8(?)以前では両方が文字列でも構わなかったらしい
  • マッチ演算子は、マッチが起こった文字の位置を返す
$ irb
>> name = "Fats Waller"
=> "Fats Waller"
>> name =~ /F/
=> 0
>> name =~ /a/
=> 1
>> name =~ /z/
=> nil
>> name =~ /f/
=> nil
>> /a/ =~ name
=> 1
  • マッチの副作用として、特殊な変数に値が設定される
    • $&:マッチ部分の文字列
    • $`:マッチ部分の前の文字列
    • $':マッチ部分の後の文字列
  • これを利用して、特定のパターンマッチの様子を表すメソッドが書ける
# 特定のパターンマッチの様子を表すメソッド
def show_regexp(a, re)
  if (a =~ re)
    "#{$`}<<#{$&}>>#{$'}"
  else
    "no match"
  end
end

p show_regexp('very interesting', /t/)
p show_regexp('Fats Waller', /a/)
p show_regexp('Fats Waller', /ll/)
p show_regexp('Fats Waller', /z/)

実行結果
"very in<<t>>eresting"
"F<<a>>ts Waller"
"Fats Wa<<ll>>er"
"no match"


今日はここまで。
再開。(2007年9月2日(日))

パターン 意味
^ 行頭(文字列リテラル中の終端またはそれぞれの\nまでを1行とみなす)
\A 行頭(文字列リテラル全体を一続きの文字列として扱う)
$ 行末(文字列リテラル中の終端またはそれぞれの\nまでを1行とみなす)
\z 又は \Z 行末(文字列リテラル全体を一続きの文字列として扱う)
p show_regexp("this is\nthe time", /is$/)
p show_regexp("this is\nthe time", /is\z/)
print "\n"
p show_regexp("this is the time\nfor me\nto buy windows ME", /me$/)
p show_regexp("this is the time\nfor me\nto buy windows ME", /me\z/)
p show_regexp("this is the time\nfor me\nto buy windows ME", /ME$/)
p show_regexp("this is the time\nfor me\nto buy windows ME", /ME\z/)

実行結果
"this is\n<<the>> time"
"this <<is>>\nthe time"
"no match"

"this is the ti<<me>>\nfor me\nto buy windows ME"
"no match"
"this is the time\nfor me\nto buy windows <<ME>>"
"this is the time\nfor me\nto buy windows <<ME>>"


今日はここまで。最近ペースが落ちてきたけど気にしないことにする。
再開。9月14日(金)

  • 文字クラス[文字]は、角括弧内の任意の1文字にマッチする
    • 特殊正規表現文字 .|()[{+^$*? は各括弧内では通常の文字とみなされる
    • POSIX 文字クラスの [:punct:] の動作がよく意味が分からない
p show_regexp("Price $12.", /[:punct:]/)    #=>    "Pri<<c>>e $12."
# "Price $12<<.>>" となると思ったんだけどな
  • 各括弧外のピリオド(.)は、改行を除くすべての文字を表す
  • 繰り返し
r*         rの0回以上の繰り返し
r+         rの1回以上の繰り返し
r?         rの0回または1回
r{m,n}     rの最低 m 回、最高 n 回の繰り返し
r{m,}      rの最低 m 回の繰り返し
r{m}       rの丁度 m 回の繰り返し
  • これらのパターンは、デフォルトではできるだけ長い文字列とマッチする
  • ただし、? を付ければ、最小回数にマッチするようになる
p show_regexp(a, /\w+/)
p show_regexp(a, /\s.*\s/)
p show_regexp(a, /\s.*?\s/)
p show_regexp(a, /[aeiou]{2,99}/)
p show_regexp(a, /mo?o/)

実行結果
"<<The>> moon is made of cheese"
"The<< moon is made of >>cheese"
"The<< moon >>is made of cheese"
"The m<<oo>>n is made of cheese"
"The <<moo>>n is made of cheese"

疑問点
# r{m,n} のカンマの後に空白を入れると no match になってしまう。仕様?
p show_regexp(a, /[aeiou]{2, 99}/)    #=>    "no match"
# r{m,} のカンマの後に空白を入れても no match になってしまう。仕様?
p show_regexp(a, /[aeiou]{2, }/)    #=>    "no match"
  • 選択
    • | 演算子の優先順位が低いことに注意
a = "red ball blue sky"
p show_regexp(a, /d|e/)
p show_regexp(a, /al|lu/)

# red ball sky でも red angry sky でも無く、
# red ball 又は angry sky となることに注意
p show_regexp(a, /red ball|angry sky/)

実行結果
"r<<e>>d ball blue sky"
"red b<<al>>l blue sky"
"<<red ball>> blue sky"
  • 括弧を使えば、正規表現内の語句をグループ化できる
a = "red ball blue sky"
p show_regexp(a, /(blue|red) \w+/)
a = "the red angry sky"
p show_regexp(a, /red (ball|angry) sky/)

実行結果
"<<red ball>> blue sky"
"the <<red angry sky>>"
  • 括弧はパターンマッチングの結果を保存する
  • 同じパターン内では、\1で1つ目のマッチを参照できる
  • パターン外では、特殊変数 $1、$2、...が同じ目的に使える
p "12:50am" =~ /(\d\d):(\d\d)(..)/
p "Hour is #$1, minute #$2"
p "12:50am" =~ /((\d\d):(\d\d))(..)/
p "Time is #$1"
p "Hour is #$2, minute #$3"
p "AM/PM is " + "#$4".upcase

出力結果
0
"Hour is 12, minute 50"
0
"Time is 12:50"
"Hour is 12, minute 50"
"AM/PM is AM"
  • 現在のマッチの一部をそのマッチ内の後方部分で使用してみる
# 重複する文字にマッチ
p show_regexp('He said "Hello"', /(\w)\1/)
# 重複する部分文字列にマッチ
p show_regexp('Mississippi', /(\w+)\1/)
# " 又は ' で囲まれた部分文字列にマッチ
p show_regexp('He said "Hello"', /(["']).*?\1/)
p show_regexp("He said 'Hello'", /(["']).*?\1/)

実行結果
"He said \"He<<ll>>o\""
"M<<ississ>>ippi"
"He said <<\"Hello\">>"
"He said <<'Hello'>>"


今日はここまで。
再開。2007年9月15日(土)

  • パターンに基づく置換
単語の先頭文字を大文字に変換する
def mixed_case(name)
  # 単語境界の後に英数字又は_が続く部分を検索・置換
  # \b:単語境界(\w と \W の間にマッチ)
  name.gsub(/\b\w/) {|first| first.upcase}
end

p mixed_case("fats waller")            #=>    "Fats Waller"
p mixed_case("louis armstrong")        #=>    "Louis Armstrong"
p mixed_case("strength in numbers")    #=>    "Strength In Numbers"
  • バックスラッシュシーケンスによる置き換え
gsub の第2引数で使えるバックスラッシュシーケンス
記号 意味
\& 最後のマッチ
\+ 最後にマッチしたグループ
\` マッチの前の文字列
\' マッチの後の文字列
\\ リテラルのバックスラッシュ(バックスラッシュそのもの)
str = 'a\b\c'
puts str.gsub(/\\/, '\\\\')
puts str.gsub(/\\/, '\\\\\\\\')
puts str.gsub(/\\/, '\&\&')
puts
p str.gsub(/\\/, '\\\\')
p str.gsub(/\\/, '\\\\\\\\')
p str.gsub(/\\/, '\&\&')

実行結果
a\b\c
a\\b\\c
a\\b\\c

"a\\b\\c"
"a\\\\b\\\\c"
"a\\\\b\\\\c"


なんか、すごい混乱する…。Ruby Reference Manual - るりまによると、

メソッド p は `\' を `\\' と出力することにも注意してください。


だそうだ。また、

慣れないうちはブロックを使います。
第二引数はあくまでも短く書くためのものだと割り切った方が良いでしょう。


とあるので、ブロックを使うことにしよう。

str = 'a\b\c'
# \& は置換文字列(gsubの第2引数)のときのみ評価されるみたい
puts str.gsub(/\\/) {"\&\&"}     #=>    a&&b&&c
puts str.gsub(/\\/) {'\&\&'}     #=>    a\&\&b\&\&c

# 無難に以下のようにしたほうが良いかな
puts str.gsub(/\\/) {'\\\\'}     #=>    a\\b\\c
puts str.gsub(/(\\)/) {"#$1#$1"} #=>    a\\b\\c

まとめ

正規表現の話がちょっと難しく感じた。詳説 正規表現 第2版で詳しく調べながら勉強したほうが良いかもしれない。しかし、けっこう突っ込んだ話を扱っていながら、本書の索引とネット上のリファレンスなどを参考にしながら読めばそれほど難なく(結構楽しく)読み進められる構成はすごいと思った。今のところ、個人的にはすごいコストパフォーマンスの良い本だと思う。