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