Text Array Format / Ruby

d:id:rubikitch:20070831, d:id:rubikitch:20070901より考えていたText Array Formatのライブラリを作成した。どういうインターフェースにするか悩んだが、結局サブクラス+DSLの形にした。

たとえばこんな内容のファイルがあったとして、

test
hogehoge
9999
!

testと!が改行なし、hogehogeが改行あり、9999が数値として扱うためには、こんなサブクラスを作成する。フィールドは上から順になっている。

class Dat2 < TAH
  field(:a) {|x| x.chomp }
  field :b
  field(:c) {|x| x.to_i }
  chomp_field(:d)
end

もちろん field(:a) {|x| x.chomp } は chomp_field(:a) とも書ける。

field/chomp_fieldで定義したあとは、ファイル名かIOオブジェクト(つーかreadとgetsを受け取るオブジェクト)を渡してparse。そしたら、read-only構造体のようにアクセスできる。

dat = Dat2.parse(filename_or_io)
dat.a                           # => "test"
dat.b                           # => "hogehoge\n"
dat.c                           # => 9999
dat.d                           # => "!"

XMLYAMLじゃoverkillな場合に便利かもしれない。

以下ソース(tah.rb)。テストはrbtest形式(rcodetoolsに同梱)で埋め込んでいる。

#!/usr/bin/env ruby
=begin rbtest
require 'stringio'
=end


class TAH
=begin test_tah_array
sio = StringIO.new(<<XXXX)
1
2
XXXX
assert_equal(["1\n", "2\n"], TAH.array(sio))
=end
  def self.array(file_or_io)
    block = lambda do |f|
      delimiter = Regexp.union(f.gets)
      f.read.split(delimiter)
    end

    if file_or_io.respond_to?(:gets) and file_or_io.respond_to?(:read)
      block[file_or_io]
    else
      open(file_or_io.to_s, &block)
    end
  end

=begin test_tah_field
eval <<XXXX
class Dat1 < TAH
  $_proc_a=lambda{|x| x.chomp }
  field :a, &$_proc_a
  field :b
end
XXXX
assert_equal [[:a, $_proc_a], [:b, nil]], Dat1.instance_variable_get(:@fields)
=end
  def self.field(name, &init)
    (@fields||=[]) << [name, init]
  end

  CHOMP_PROC = lambda{|x| x.chomp }
  def self.chomp_field(name)
    field(name, &CHOMP_PROC)
  end
  def self.field_chomp(name)
    chomp_field(name)
  end

  def self.fields
    @fields
  end

=begin test_tah_parse1
eval <<XXXX
class Dat2 < TAH
  field(:a) {|x| x.chomp }
  field :b
  field(:c) {|x| x.to_i }
  chomp_field(:d)
end
XXXX
sio = StringIO.new <<XXXX
test
hogehoge
9999
!
XXXX
dat = Dat2.parse(sio)
assert_equal( ["test", "hogehoge\n", 9999, "!"], [dat.a, dat.b, dat.c, dat.d] )
=end
  def self.parse(file_or_io)
    obj = new
    eigenclass = class << obj; self end

    array(file_or_io).zip(fields).each do |str, (name, init)|
      str = init[str] if init
      eigenclass.module_eval do
        define_method(name) { str }
      end
    end
    obj
  end

end