MultiTextFormat

るびきちさんのブログでTextArrayFormatのアイディアを見て(実装は見ず)真似をしてみた。
理由はわからない。が、きっともっと難しいモノだったら取り組んでないから自己満足であろう。


実際に実装を見てみたら、あたりまえだけど細かいところは違った。
自分の「内部で継承+DSLをclass_eval」でユーザに直接継承を書かせない感じのやり方は、多分にるびまの青木さんのコード添削の記事に影響を受けている気がする。
あれでびっくりしたのがキッカケでeval系とかBindingとかについてちゃんと考えるようになったからなぁ…。


■追記

ざっと見直したら気に食わない名前とか1.9だと駄目なところとかが
あったので後で直す。

■直した

  • DEFAULT_PARSE -> DEFAULT_FIELD_PROC
  • @parsing_methods -> @parsing_procs
  • text.each -> text.lines.each (+ Ruby<1.9 用のString拡張)

実装

require 'stringio'


#for backward Ruby1.9
class String
  unless respond_to?(:lines)
    def lines
      self
    end
  end
end



class MultiTextFormat
  private_class_method :new
  
  DEFAULT_FIELD_PROC = lambda{|x| x }
  
  def self.define(&block)
    Class.new(self) do
      @parsing_order, @parsing_procs = [], {}
      private_class_method :define
      public_class_method :parse
      class_eval(&block)
      [@parsing_order, @parsing_procs].each{|x| x.freeze }
      def self.parsing_order ; @parsing_order ; end
    end
  end
  
  def self.field(name, &block)
    name = name.to_sym
    @parsing_order << name
    @parsing_procs[name] = block || DEFAULT_FIELD_PROC
    attr_reader name
  end
  
  def self.array_field(name, &block)
    block = block || DEFAULT_FIELD_PROC
    field(name){|text|
      array = []
      text.lines.each{|line| array << block.call(line) }
      array
    }
  end
  
  def self.parse(f)
    mtext = new()
    f = StringIO.new(f) unless f.respond_to?(:gets)
    if delimiter = f.gets
      data = f.read.split(/^#{Regexp.escape(delimiter)}/)
      @parsing_order.zip(data) do |field_name, text|
        mtext.instance_variable_set(
          "@#{field_name}", @parsing_procs[field_name].call(text)
        )
      end
    end
    mtext
  end
  private_class_method :parse
  
end

テスト

require 'test/unit'
require 'multitextformat'


class TC_MTF_interface < Test::Unit::TestCase
  def test_baseclass_interface
    assert_raise(NoMethodError){ MultiTextFormat.new }
    assert_respond_to(MultiTextFormat, :define)
  end
  
  def test_createdclass_interface
    list = [:hoge1, :hoge2, :hoge3]
    fclass = MultiTextFormat.define{
      list.each{|name| field(name) }
    }
    assert_raise(NoMethodError){ fclass.new }
    assert_raise(NoMethodError){ fclass.define }
    assert_respond_to(fclass, :parse)
  end
  
  def test_instance_interface
    list = [:hoge1, :hoge2, :hoge3]
    fclass = MultiTextFormat.define{
      list.each{|name| field(name) }
    }
    mtext = fclass.parse('')
    list.each do |name|
      assert_respond_to(mtext, name)
    end
  end
end


class TC_MTF < Test::Unit::TestCase
  TEST_TEXT = <<-_Eos_
hoge
piyo
1234
line-A
line-B
1000
2000
line1
line2
=======
line3
  _Eos_
  
  FCLASS = MultiTextFormat.define{
    field(:text1)
    field(:num1){|x| x.to_i }
    field(:text2){|x| x.chomp }
    array_field(:nums){|x| x.to_i }
    array_field(:lines){|x| x.chomp }
  }
  
  def setup
    @t = FCLASS.parse(TEST_TEXT)
  end
  
  def test_field_text1
    assert_equal(@t.text1, "hoge\npiyo\n")
  end
  
  def test_field_num1
    assert_equal(@t.num1, 1234)
  end
  
  def test_field_text2
    assert_equal(@t.text2, "line-A\nline-B")
  end
  
  def test_array_field_nums
    assert_equal(@t.nums, [1000, 2000])
  end
  
  def test_array_field_lines
    assert_equal(
      @t.lines,
      %w[
        line1
        line2
        =======
        line3
      ]
    )
  end
  
end