fixed_width 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.gem
2
+ .eprj
3
+ pkg
4
+ coverage
5
+ doc
data/COPYING ADDED
@@ -0,0 +1,10 @@
1
+ Copyright (c) 2010, Topspin Media Inc.
2
+ All rights reserved.
3
+
4
+ Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
5
+
6
+ * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
7
+ * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.
8
+ * Neither the name of the Topspin Media Inc. nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission.
9
+
10
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
data/HISTORY ADDED
@@ -0,0 +1,12 @@
1
+ v0.1.1 (2010-05-29)
2
+ =========================
3
+ * column grouping (parsing and writing)
4
+ * :singular section option
5
+ * :nil_blank column option
6
+ * some rdoc for FixedWidth
7
+ * idiomatic syntax cleanup
8
+
9
+ v0.1.0 (2010-05-19)
10
+ =========================
11
+ * non-release (hence untagged)
12
+ * Forked from ryanwood/slither, renamed.
data/README.markdown ADDED
@@ -0,0 +1,149 @@
1
+ DESCRIPTION:
2
+ ============
3
+
4
+ A simple, clean DSL for describing, writing, and parsing fixed-width text files.
5
+
6
+ FEATURES:
7
+ =========
8
+
9
+ * Easy DSL syntax
10
+ * Can parse and format fixed width files
11
+ * Templated sections for reuse
12
+
13
+ SYNOPSIS:
14
+ =========
15
+
16
+ ##Creating a definition (Quick 'n Dirty)
17
+
18
+ Hopefully this will cover 90% of use cases.
19
+
20
+ # Create a FixedWidth::Defintion to describe a file format
21
+ FixedWidth.define :simple do |d|
22
+ # This is a template section that can be reused in other sections
23
+ d.template :boundary do |t|
24
+ t.column :record_type, 4
25
+ t.column :company_id, 12
26
+ end
27
+
28
+ # Create a section named :header
29
+ d.header(:align => :left) do |header|
30
+ # The trap tells FixedWidth which lines should fall into this section
31
+ header.trap { |line| line[0,4] == 'HEAD' }
32
+ # Use the boundary template for the columns
33
+ header.template :boundary
34
+ end
35
+
36
+ d.body do |body|
37
+ body.trap { |line| line[0,4] =~ /[^(HEAD|FOOT)]/ }
38
+ body.column :id, 10, :parser => :to_i
39
+ body.column :first, 10, :align => :left, :group => :name
40
+ body.column :last, 10, :align => :left, :group => :name
41
+ body.spacer 3
42
+ body.column :city, 20 , :group => :address
43
+ body.column :state, 2 , :group => :address
44
+ body.column :country, 3, :group => :address
45
+ end
46
+
47
+ d.footer do |footer|
48
+ footer.trap { |line| line[0,4] == 'FOOT' }
49
+ footer.template :boundary
50
+ footer.column :record_count, 10, :parser => :to_i
51
+ end
52
+ end
53
+
54
+ This definition would output a parsed file something like this:
55
+
56
+ {
57
+ :body => [
58
+ { :id => 12,
59
+ :name => { :first => "Ryan", :last => "Wood" },
60
+ :address => { :city => "Foo", :state => 'SC', :country => "USA" }
61
+ },
62
+ { :id => 13,
63
+ :name => { :first => "Jo", :last => "Schmo" },
64
+ :address => { :city => "Bar", :state => "CA", :country => "USA" }
65
+ }
66
+ ],
67
+ :header => [{ :record_type => 'HEAD', :company_id => 'ABC' }],
68
+ :footer => [{ :record_type => 'FOOT', :company_id => 'ABC', :record_count => 2 }]
69
+ }
70
+
71
+ ##Sections
72
+ ###Declaring a section
73
+
74
+ Sections can have any name, however duplicates are not allowed. (A `DuplicateSectionNameError` will be thrown.) We use the standard `method_missing` trick. So if you see any unusual behavior, that's probably the first spot to look.
75
+
76
+ FixedWidth.define :simple do |d|
77
+ d.a_section_name do |s|
78
+ ...
79
+ end
80
+ d.another_section_name do |s|
81
+ ...
82
+ end
83
+ end
84
+
85
+ ### Section options:
86
+
87
+ * `:singular` (default `false`) indicates that the section will only have a single record, and that it should not be returned nested in an array.
88
+
89
+ * `:optional` (default `false`) indicates that the section is optional. (An otherwise-specified section will raise a `RequiredSectionNotFoundError` if the trap block doesn't match the row after the last one of the previous section.)
90
+
91
+ ##Columns
92
+ ###Declaring a column
93
+
94
+ Columns can have any name, except for `:spacer` which is reserved. Also, duplicate column names within groupings are not allowed, and a column cannot share the same name as a group. (A `DuplicateColumnNameError` will be thrown for a duplicate column name within a grouping. A `DuplicateGroupNameError` will be thrown if you try to declare a column with the same name as an existing group or vice versa.) Again, basic `method_missing` trickery here, so be warned. You can declare columns either with the `method_missing` thing or by calling `Section#column`.
95
+
96
+ FixedWidth.define :simple do |d|
97
+ d.a_section_name do |s|
98
+ s.a_column_name 12
99
+ s.column :another_column_name, 14
100
+ end
101
+ end
102
+
103
+ ###Column Options:
104
+
105
+ * `:align` can be set to `:left` or `:right`, to indicate which side the values should be/are justified to. By default, all columns are aligned `:right`.
106
+
107
+ * `:group` can be set to a `Symbol` indicating the name of the nested hash which the value should be parsed to when reading/the name of the nested hash the value should be extracted from when writing.
108
+
109
+ * `:parser` and `:formatter` options are symbols (to be proc-ified) or procs. By default, parsing and formatting assume that we're expecting/writing right-aligned strings, padded with spaces.
110
+
111
+ * `:nil_blank` set to true will cause whitespace-only fields to be parsed to nil, regardless of `:parser`.
112
+
113
+ * `:padding` can be set to a single character that will be used to pad formatted values, when writing fixed-width files.
114
+
115
+ * `:truncate` can be set to true to truncate any value that exceeds the `length` property of a column. If unset or set to `false`, a `FixedWidth::FormattedStringExceedsLengthError` exception will be thrown.
116
+
117
+ ##Writing out fixed-width records
118
+
119
+ Then either feed it a nested struct with data values to create the file in the defined format:
120
+
121
+ test_data = {
122
+ :body => [
123
+ { :id => 12,
124
+ :name => { :first => "Ryan", :last => "Wood" },
125
+ :address => { :city => "Foo", :state => 'SC', :country => "USA" }
126
+ },
127
+ { :id => 13,
128
+ :name => { :first => "Jo", :last => "Schmo" },
129
+ :address => { :city => "Bar", :state => "CA", :country => "USA" }
130
+ }
131
+ ],
132
+ :header => [{ :record_type => 'HEAD', :company_id => 'ABC' }],
133
+ :footer => [{ :record_type => 'FOOT', :company_id => 'ABC', :record_count => 2 }]
134
+ }
135
+
136
+ # Generates the file as a string
137
+ puts FixedWidth.generate(:simple, test_data)
138
+
139
+ # Writes the file
140
+ FixedWidth.write(file_instance, :simple, test_data)
141
+
142
+ Or parse files already in that format into a nested hash:
143
+
144
+ parsed_data = FixedWidth.parse(file_instance, :test).inspect
145
+
146
+ INSTALL:
147
+ ========
148
+
149
+ sudo gem install fixed_width
data/Rakefile ADDED
@@ -0,0 +1,44 @@
1
+ task :default => :spec
2
+ task :test => :spec
3
+
4
+ desc "Build a gem"
5
+ task :gem => [ :gemspec, :build ]
6
+
7
+ desc "Run specs"
8
+ task :spec do
9
+ exec "spec -fn -b -c spec/"
10
+ end
11
+
12
+ begin
13
+ require 'jeweler'
14
+ Jeweler::Tasks.new do |gemspec|
15
+ gemspec.name = "fixed_width"
16
+ gemspec.summary = "A gem that provides a DSL for parsing and writing files of fixed-width records."
17
+ gemspec.description = <<END
18
+ Shamelessly forked from ryanwood/slither [http://github.com/ryanwood/slither].
19
+
20
+ Renamed the gem to be a little clearer as to its purpose. Hate that 'nokogiri' nonsense.
21
+ END
22
+ gemspec.email = "timon.karnezos@gmail.com"
23
+ gemspec.homepage = "http://github.com/timonk/fixed_width"
24
+ gemspec.authors = ["Timon Karnezos"]
25
+ end
26
+ rescue LoadError
27
+ warn "Jeweler not available. Install it with:"
28
+ warn "gem install jeweler"
29
+ end
30
+
31
+ require 'rake/rdoctask'
32
+ Rake::RDocTask.new do |rdoc|
33
+ if File.exist?('VERSION')
34
+ version = File.read('VERSION')
35
+ else
36
+ version = ""
37
+ end
38
+
39
+ rdoc.rdoc_dir = 'rdoc'
40
+ rdoc.title = "rprince #{version}"
41
+ rdoc.options << '--line-numbers' << '--inline-source'
42
+ rdoc.rdoc_files.include('README*')
43
+ rdoc.rdoc_files.include('lib/**/*.rb')
44
+ end
data/TODO ADDED
@@ -0,0 +1,7 @@
1
+ * Add :limit option on sections
2
+ * Add :validation option for columns
3
+ * Add a validate_file() method to parse a file and run all validation tests (implies validation implemented)
4
+ * Better Documentation
5
+ * Alternate Section Flow (other than linear), i.e. repeatable sections (think batch)
6
+ * Add batched generation, so we don't turn into memory hogs
7
+ * Add batched parsing, so we don't turn into memory hogs
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,62 @@
1
+ require 'stringio'
2
+ require File.join(File.dirname(__FILE__), "..", "lib", "fixed_width")
3
+
4
+ # Create a FixedWidth::Defintion to describe a file format
5
+ FixedWidth.define :simple do |d|
6
+ # This is a template section that can be reused in other sections
7
+ d.template :boundary do |t|
8
+ t.column :record_type, 4
9
+ t.column :company_id, 12
10
+ end
11
+
12
+ # Create a header section
13
+ d.header(:align => :left) do |header|
14
+ # The trap tells FixedWidth which lines should fall into this section
15
+ header.trap { |line| line[0,4] == 'HEAD' }
16
+ # Use the boundary template for the columns
17
+ header.template :boundary
18
+ end
19
+
20
+ d.body do |body|
21
+ body.trap { |line| line[0,4] =~ /[^(HEAD|FOOT)]/ }
22
+ body.column :id, 10, :parser => :to_i
23
+ body.column :first, 10, :align => :left, :group => :name
24
+ body.column :last, 10, :align => :left, :group => :name
25
+ body.spacer 3
26
+ body.column :city, 20 , :group => :address
27
+ body.column :state, 2 , :group => :address
28
+ body.column :country, 3, :group => :address
29
+ end
30
+
31
+ d.footer do |footer|
32
+ footer.trap { |line| line[0,4] == 'FOOT' }
33
+ footer.template :boundary
34
+ footer.column :record_count, 10, :parser => :to_i
35
+ end
36
+ end
37
+
38
+ test_data = {
39
+ :body => [
40
+ { :id => 12,
41
+ :name => { :first => "Ryan", :last => "Wood" },
42
+ :address => { :city => "Foo", :state => 'SC', :country => "USA" }
43
+ },
44
+ { :id => 13,
45
+ :name => { :first => "Jo", :last => "Schmo" },
46
+ :address => { :city => "Bar", :state => "CA", :country => "USA" }
47
+ }
48
+ ],
49
+ :header => [{ :record_type => 'HEAD', :company_id => 'ABC' }],
50
+ :footer => [{ :record_type => 'FOOT', :company_id => 'ABC', :record_count => 2 }]
51
+ }
52
+
53
+ # Generates the file as a string
54
+ generated = FixedWidth.generate(:simple, test_data)
55
+
56
+ sio = StringIO.new
57
+ sio.write(generated)
58
+ sio.rewind
59
+
60
+ parsed = FixedWidth.parse(sio, :simple)
61
+
62
+ puts parsed == test_data
@@ -0,0 +1,74 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE DIRECTLY
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run the gemspec command
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{fixed_width}
8
+ s.version = "0.2.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Timon Karnezos"]
12
+ s.date = %q{2010-05-31}
13
+ s.description = %q{Shamelessly forked from ryanwood/slither [http://github.com/ryanwood/slither].
14
+
15
+ Renamed the gem to be a little clearer as to its purpose. Hate that 'nokogiri' nonsense.
16
+ }
17
+ s.email = %q{timon.karnezos@gmail.com}
18
+ s.extra_rdoc_files = [
19
+ "README.markdown",
20
+ "TODO"
21
+ ]
22
+ s.files = [
23
+ ".gitignore",
24
+ "COPYING",
25
+ "HISTORY",
26
+ "README.markdown",
27
+ "Rakefile",
28
+ "TODO",
29
+ "VERSION",
30
+ "examples/readme_example.rb",
31
+ "fixed_width.gemspec",
32
+ "lib/fixed_width.rb",
33
+ "lib/fixed_width/column.rb",
34
+ "lib/fixed_width/core_ext/symbol.rb",
35
+ "lib/fixed_width/definition.rb",
36
+ "lib/fixed_width/fixed_width.rb",
37
+ "lib/fixed_width/generator.rb",
38
+ "lib/fixed_width/parser.rb",
39
+ "lib/fixed_width/section.rb",
40
+ "spec/column_spec.rb",
41
+ "spec/definition_spec.rb",
42
+ "spec/fixed_width_spec.rb",
43
+ "spec/generator_spec.rb",
44
+ "spec/parser_spec.rb",
45
+ "spec/section_spec.rb",
46
+ "spec/spec_helper.rb"
47
+ ]
48
+ s.homepage = %q{http://github.com/timonk/fixed_width}
49
+ s.rdoc_options = ["--charset=UTF-8"]
50
+ s.require_paths = ["lib"]
51
+ s.rubygems_version = %q{1.3.6}
52
+ s.summary = %q{A gem that provides a DSL for parsing and writing files of fixed-width records.}
53
+ s.test_files = [
54
+ "spec/column_spec.rb",
55
+ "spec/definition_spec.rb",
56
+ "spec/fixed_width_spec.rb",
57
+ "spec/generator_spec.rb",
58
+ "spec/parser_spec.rb",
59
+ "spec/section_spec.rb",
60
+ "spec/spec_helper.rb",
61
+ "examples/readme_example.rb"
62
+ ]
63
+
64
+ if s.respond_to? :specification_version then
65
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
66
+ s.specification_version = 3
67
+
68
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
69
+ else
70
+ end
71
+ else
72
+ end
73
+ end
74
+
@@ -0,0 +1,85 @@
1
+ class FixedWidth
2
+ class Column
3
+ DEFAULT_PADDING = ' '
4
+ DEFAULT_ALIGNMENT = :right
5
+ DEFAULT_TRUNCATE = false
6
+ DEFAULT_FORMATTER = :to_s
7
+
8
+ attr_reader :name, :length, :alignment, :padding, :truncate, :group, :unpacker
9
+
10
+ def initialize(name, length, options={})
11
+ assert_valid_options(options)
12
+ @name = name
13
+ @length = length
14
+ @alignment = options[:align] || DEFAULT_ALIGNMENT
15
+ @padding = options[:padding] || DEFAULT_PADDING
16
+ @truncate = options[:truncate] || DEFAULT_TRUNCATE
17
+
18
+ @group = options[:group]
19
+
20
+ @unpacker = "A#{@length}"
21
+
22
+ @parser = options[:parser]
23
+ @parser ||= case @alignment
24
+ when :right then :lstrip
25
+ when :left then :rstrip
26
+ end
27
+ @parser = @parser.to_proc if @parser.is_a?(Symbol)
28
+
29
+ @formatter = options[:formatter]
30
+ @formatter ||= DEFAULT_FORMATTER
31
+ @formatter = @formatter.to_proc if @formatter.is_a?(Symbol)
32
+
33
+ @nil_blank = options[:nil_blank]
34
+ end
35
+
36
+ def parse(value)
37
+ if @nil_blank && blank?(value)
38
+ return nil
39
+ else
40
+ @parser.call(value)
41
+ end
42
+ rescue
43
+ raise ParserError.new("The value '#{value}' could not be parsed: #{$!}")
44
+ end
45
+
46
+ def format(value)
47
+ pad(
48
+ validate_size(
49
+ @formatter.call(value)
50
+ )
51
+ )
52
+ end
53
+
54
+ private
55
+ BLANK_REGEX = /^\s*$/
56
+ def blank?(value)
57
+ value =~ BLANK_REGEX
58
+ end
59
+
60
+ def pad(value)
61
+ case @alignment
62
+ when :left
63
+ value.ljust(@length, @padding)
64
+ when :right
65
+ value.rjust(@length, @padding)
66
+ end
67
+ end
68
+
69
+ def assert_valid_options(options)
70
+ unless options[:align].nil? || [:left, :right].include?(options[:align])
71
+ raise ArgumentError.new("Option :align only accepts :right (default) or :left")
72
+ end
73
+ end
74
+
75
+ def validate_size(result)
76
+ return result if result.length <= @length
77
+ raise FixedWidth::FormattedStringExceedsLengthError.new(
78
+ "The formatted value '#{result}' in column '#{@name}' exceeds the allowed length of #{@length} chararacters.") unless @truncate
79
+ case @alignment
80
+ when :right then result[-@length,@length]
81
+ when :left then result[0,@length]
82
+ end
83
+ end
84
+ end
85
+ end