diff --git a/README.rdoc b/README.rdoc old mode 100644 new mode 100755 index 2cbc428..1a4b1a1 --- a/README.rdoc +++ b/README.rdoc @@ -70,6 +70,38 @@ or parse files already in that format into a nested hash: parsed_data = Slither.parse('infile.txt', :test).inspect +== Repeating Sections + +To enable repeating sections simple define the prefix you want for the repeating keys in the hash and pass the +repeatable option to the section that is repeatable. + + Slither.define :sac_bee, :repeater => 'r' do |d| + d.header do |h| + h.trap { |line| line[0] == '1' } + h.column :header_begin, 1 + h.column :destination + h.column :payment_deposit_date, 6 + end + d.payment_records :repeatable => true do |pr| + pr.trap { |line| line[0] == '6' } + pr.column :payment_amount, 10 + end + d.batch_trailer_record :repeatable => true do |btr| + btr.trap { |line| line[0] == '7' } + btr.column :beginning_of_batch_trailer_record, 1 + btr.column :total_count_of_records_in_batch, 4 + btr.column :total_payment_amount_of_batch, 10 + end + d.file_trailer_record do |ftr| + ftr.trap { |line| line[0] == '8' } + ftr.column :beginning_of_trailer_record, 1 + ftr.column :total_count_of_records, 5 + ftr.column :total_payment_amount, 10 + end + end + +The data will be parsed into a hash as above but will have keys like "payment_records#{repeater}{num}" for each repeating section. + == INSTALL: sudo gem install slither diff --git a/lib/slither/column.rb b/lib/slither/column.rb old mode 100644 new mode 100755 diff --git a/lib/slither/definition.rb b/lib/slither/definition.rb old mode 100644 new mode 100755 index 9158203..cee2402 --- a/lib/slither/definition.rb +++ b/lib/slither/definition.rb @@ -1,10 +1,11 @@ class Slither class Definition - attr_reader :sections, :templates, :options + attr_reader :sections, :templates, :options, :repeater def initialize(options = {}) @sections = [] @templates = {} + @repeater = options[:repeater] || 'r' @options = { :align => :right }.merge(options) end @@ -26,6 +27,18 @@ def template(name, options = {}, &block) @templates[name] = section end + def find_section(name) + @sections.select{|section| section.name.to_sym == name}.first rescue nil + end + + def repeatable_sections + @sections.select{|section| section.repeatable} + end + + def non_repeatable_sections + @sections.select{|section| !section.repeatable} + end + def method_missing(method, *args, &block) section(method, *args, &block) end diff --git a/lib/slither/generator.rb b/lib/slither/generator.rb old mode 100644 new mode 100755 index a2a54bb..e7b5f95 --- a/lib/slither/generator.rb +++ b/lib/slither/generator.rb @@ -7,17 +7,19 @@ def initialize(definition) def generate(data) @builder = [] - @definition.sections.each do |section| - content = data[section.name] - if content - content = [content] unless content.is_a?(Array) - raise(Slither::RequiredSectionEmptyError, "Required section '#{section.name}' was empty.") if content.empty? - content.each do |row| - @builder << section.format(row) - end - else - raise(Slither::RequiredSectionEmptyError, "Required section '#{section.name}' was empty.") unless section.optional - end + data.each do |section_name,content| + #remove #{@definition.repeater}[number] which was created by repeating rows + repeat_regex = Regexp.new("#{@definition.repeater}{1}\\d+\\z") + section_name = section_name.to_s.gsub(repeat_regex, '') + + section = @definition.find_section(section_name.to_sym) + raise(Slither::UndefinedSectionError, "Undefined section '#{section_name}'.") if section.nil? + + content = [content] unless content.is_a?(Array) + raise(Slither::UknownSectionError, "Required section '#{section.name}' was empty.") if content.empty? + content.each do |row| + @builder << section.format(row) + end end @builder.join("\n") end diff --git a/lib/slither/parser.rb b/lib/slither/parser.rb old mode 100644 new mode 100755 index 0582ba2..db24810 --- a/lib/slither/parser.rb +++ b/lib/slither/parser.rb @@ -3,6 +3,8 @@ class Parser def initialize(definition, file) @definition = definition + @repeat_counter = 1 + @repeating_section = nil @file = file # This may be used in the future for non-linear or repeating sections @mode = :linear @@ -12,9 +14,22 @@ def parse() @parsed = {} @content = read_file unless @content.empty? - @definition.sections.each do |section| + @definition.non_repeatable_sections.each do |section| rows = fill_content(section) - raise(Slither::RequiredSectionNotFoundError, "Required section '#{section.name}' was not found.") unless rows > 0 || section.optional + + #if no matches were found this might be a repeatable section + if rows == 0 + repeatable_rows = parse_repeatable_sections + while repeatable_rows > 0 + repeatable_rows = parse_repeatable_sections + end + rows = fill_content(section) + end + + if rows == 0 and !section.optional + raise(Slither::RequiredSectionNotFoundError, "Required section '#{section.name}' was not found.") + end + end end @parsed @@ -32,21 +47,39 @@ def read_file content end - def fill_content(section) + def fill_content(section, repeatable=false) matches = 0 + repeat_section_name = nil + loop do line = @content.first break unless section.match(line) - add_to_section(section, line) + if repeatable + unless @repeating_section == section.name + @repeating_section = section.name + repeat_section_name = "#{section.name}#{@definition.repeater}#{@repeat_counter}" + @repeat_counter += 1 + end + end + add_to_section(section, line, repeatable, repeat_section_name) matches += 1 @content.shift end matches end - def add_to_section(section, line) - @parsed[section.name] = [] unless @parsed[section.name] - @parsed[section.name] << section.parse(line) + def parse_repeatable_sections + matches = 0 + @definition.repeatable_sections.each do |section| + matches = fill_content(section, true) + end + matches + end + + def add_to_section(section, line, repeatable, repeat_section_name) + key = repeatable ? repeat_section_name.to_sym : section.name + @parsed[key] = [] unless @parsed[key] + @parsed[key] << section.parse(line) end end diff --git a/lib/slither/section.rb b/lib/slither/section.rb old mode 100644 new mode 100755 index 540f155..bd9cea0 --- a/lib/slither/section.rb +++ b/lib/slither/section.rb @@ -1,6 +1,6 @@ class Slither class Section - attr_accessor :definition, :optional + attr_accessor :definition, :optional, :repeatable attr_reader :name, :columns, :options RESERVED_NAMES = [:spacer] @@ -11,6 +11,7 @@ def initialize(name, options = {}) @columns = [] @trap = options[:trap] @optional = options[:optional] || false + @repeatable = options[:repeatable] || false end def column(name, length, options = {}) diff --git a/lib/slither/slither.rb b/lib/slither/slither.rb old mode 100644 new mode 100755 index c290562..d67cdcc --- a/lib/slither/slither.rb +++ b/lib/slither/slither.rb @@ -8,6 +8,8 @@ class RequiredSectionEmptyError < StandardError; end class FormattedStringExceedsLengthError < StandardError; end class ColumnMismatchError < StandardError; end + class UndefinedSectionError < StandardError; end + def self.define(name, options = {}, &block) definition = Definition.new(options) diff --git a/spec/definition_spec.rb b/spec/definition_spec.rb index 92e8770..7bc89a7 100644 --- a/spec/definition_spec.rb +++ b/spec/definition_spec.rb @@ -23,6 +23,74 @@ d.section('name', :align => :left) {} end end + + describe "when specifying repeater" do + it "should have a repeater option" do + d = Slither::Definition.new :repeater => 'r' + d.repeater.should == 'r' + end + + it "should should default repeater to r" do + d = Slither::Definition.new + d.repeater.should == 'r' + end + + it "should should allow you get all repeatable sections" do + d = Slither::Definition.new + + d.section :header do |section| + end + + d.section :body, :repeatable => true do |section| + end + + d.section :footer do |section| + end + + d.repeatable_sections.count.should eq 1 + end + + it "should should allow you get all non repeatable sections" do + d = Slither::Definition.new + + d.section :header do |section| + end + + d.section :body, :repeatable => true do |section| + end + + d.section :footer do |section| + end + + d.non_repeatable_sections.count.should eq 2 + end + + it "should should allow you get all non repeatable sections" do + d = Slither::Definition.new + + d.section :header do |section| + end + + d.section :body, :repeatable => true do |section| + end + + d.section :footer do |section| + end + + d.non_repeatable_sections.count.should eq 2 + end + + it "should allow you get a section by name" do + d = Slither::Definition.new + + d.section :header do |section| + end + + section = d.find_section(:header) + section.should be_a(Slither::Section) + section.name.should eq :header + end + end describe "when creating a section" do before(:each) do diff --git a/spec/generator_spec.rb b/spec/generator_spec.rb index c7c4d2b..807b1c0 100644 --- a/spec/generator_spec.rb +++ b/spec/generator_spec.rb @@ -38,5 +38,58 @@ it "should generate a string" do expected = "HEAD 1\n Paul Hewson\n Dave Evans\nFOOT 1" @generator.generate(@data).should == expected - end + end + + describe "when repeating sections" do + before(:all) do + @repeat_definition = Slither.define :simple, :repeater => 'r' do |d| + d.header do |header| + header.trap { |line| line[0,1] == 1 } + header.column :header_begin, 1 + header.column :batch_number, 3, :padding => :zero + end + d.data :repeatable => true do |data| + data.trap { |line| line[0,1] == 2 } + data.column :data_begin, 1 + data.column :record_number, 3 + data.column :record_number_plus_batch, 6 + data.column :id, 2 + data.column :name, 10, :align => :left + end + d.tail_record :repeatable => true do |tail_record| + tail_record.trap { |line| line[0,1] == 3 } + tail_record.column :tail_record_begin, 1 + tail_record.column :num_records, 3 + end + d.footer do |footer| + footer.trap { |line| line[0,1] == 4 } + footer.column :footer_record_begin, 1 + footer.column :total_record_count, 3 + footer.column :batch_number, 3, :padding => :zero + end + end + @repeat_data = { + :header => [ {:header_begin => 1, :batch_number => 001 }], + :datar1 => [ + {:data_begin => 2, :record_number => '001', + :record_number_plus_batch => '001001', :id => '01', :name => 'Russell' }, + {:data_begin => 2, :record_number => '002', + :record_number_plus_batch => '002001', :id => '02', :name => 'John' }, + ], + :tail_recordr1 => [:tail_record_begin => 3, :num_records => '002'], + :datar2 => [ + {:data_begin => 2, :record_number => '001', + :record_number_plus_batch => '001001', :id => '01', :name => 'Bill' } + ], + :tail_recordr2 => [:tail_record_begin => 3, :num_records => '001'], + :footer => [ {:footer_record_begin => 4, :total_record_count => "003", :batch_number => '001' }] + } + @repeat_generator = Slither::Generator.new(@repeat_definition) + end + + it "should generate a string" do + expected = "1001\n200100100101Russell \n200200200102John \n3002\n200100100101Bill \n3001\n4003001" + @repeat_generator.generate(@repeat_data).should == expected + end + end end \ No newline at end of file diff --git a/spec/parser_spec.rb b/spec/parser_spec.rb index 4debf86..0073c22 100644 --- a/spec/parser_spec.rb +++ b/spec/parser_spec.rb @@ -71,4 +71,68 @@ # it "raise an error if a section limit is over run" end + + describe "when repeating sections" do + before(:each) do + @repeat_file_name = 'repeat.txt' + end + + before(:all) do + @repeat_definition = Slither.define :simple, :repeater => 'r' do |d| + d.header do |header| + header.trap { |line| line[0] == '1' } + header.column :header_begin, 1 + header.column :batch_number, 3, :padding => :zero + end + d.data :repeatable => true do |data| + data.trap { |line| line[0] == '2' } + data.column :data_begin, 1 + data.column :record_number, 3 + data.column :record_number_plus_batch, 6 + data.column :id, 2 + data.column :name, 10, :align => :left + end + d.tail_record :repeatable => true do |tail_record| + tail_record.trap { |line| line[0] == '3' } + tail_record.column :tail_record_begin, 1 + tail_record.column :num_records, 3 + end + d.footer do |footer| + footer.trap { |line| line[0] == '4' } + footer.column :footer_record_begin, 1 + footer.column :total_record_count, 3 + footer.column :batch_number, 3, :padding => :zero + end + end + + File.open('repeat.txt', 'w') {|f| f.write("1001\n200100100101Russell \n200200200102John \n3002\n200100100101Bill \n3001\n4003001") } + @repeat_parser = Slither::Parser.new(@repeat_definition, 'repeat.txt') + end + + it "should create hash keys based on repeated sections" do + expected = { + :header => [ {:header_begin => '1', :batch_number => '001' }], + :datar1 => [ + {:data_begin => "2", :record_number => '001', + :record_number_plus_batch => '001001', :id => '01', :name => 'Russell' }, + {:data_begin => "2", :record_number => '002', + :record_number_plus_batch => '002001', :id => '02', :name => 'John' }, + ], + :tail_recordr2 => [:tail_record_begin => '3', :num_records => '002'], + :datar3 => [ + {:data_begin => "2", :record_number => '001', + :record_number_plus_batch => '001001', :id => '01', :name => 'Bill' } + ], + :tail_recordr4 => [:tail_record_begin => '3', :num_records => '001'], + :footer => [ {:footer_record_begin => '4', :total_record_count => "003", :batch_number => '001' }] + } + result = @repeat_parser.parse + result.should == expected + end + + after(:all) do + File.delete('repeat.txt') + end + + end end \ No newline at end of file diff --git a/spec/section_spec.rb b/spec/section_spec.rb index 80aab73..1029f87 100644 --- a/spec/section_spec.rb +++ b/spec/section_spec.rb @@ -4,7 +4,15 @@ before(:each) do @section = Slither::Section.new(:body) end - + + it "should have a repeatable option" do + @section.repeatable.should_not be_nil + end + + it "should default the repeatable option to false" do + @section.repeatable.should eq false + end + it "should have no columns after creation" do @section.columns.should be_empty end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 14b5e29..b7210c7 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,4 +1,4 @@ require 'rubygems' -require 'spec' +require 'rspec' require File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib', 'slither')) \ No newline at end of file