pile 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/Rakefile +15 -0
- data/lib/pile.rb +3 -0
- data/lib/pile/header.rb +116 -0
- data/lib/pile/list.rb +199 -0
- data/lib/pile/record.rb +85 -0
- data/lib/pile/version.rb +3 -0
- data/spec/header_spec.rb +104 -0
- data/spec/list_spec.rb +92 -0
- data/spec/record_spec.rb +87 -0
- data/spec/spec_helper.rb +48 -0
- metadata +74 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 19ad39412c2023f5a744a424e9b940facf3002d1
|
4
|
+
data.tar.gz: 5fc589e24e53245e1aeab8a6674a8264cd0496f6
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f07e56bc2906081397cc58329f34b78af792eab83c48c935c6537ac04eba53d4361dd0f00a65d73bf3581d441c2f8b56428cdee1945af04786668f831b49762f
|
7
|
+
data.tar.gz: 538ed3323cf0b90be97c0cddd29b0ae5fea8a50f96f35cd47707d58fd360db0dd8473e8a69e42a369a175fdb297fea4f8e26787d4b29854e0a935f9a3f43213b
|
data/Rakefile
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rspec/core/rake_task'
|
4
|
+
require 'yard'
|
5
|
+
require 'yard/rake/yardoc_task'
|
6
|
+
|
7
|
+
desc "Run rspec with formatting"
|
8
|
+
RSpec::Core::RakeTask.new(:test) do |t|
|
9
|
+
t.rspec_opts = ['--colour', '--format documentation']
|
10
|
+
t.pattern = '*_spec.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
desc "Generate YARD documentation"
|
14
|
+
YARD::Rake::YardocTask.new :doc do
|
15
|
+
end
|
data/lib/pile.rb
ADDED
data/lib/pile/header.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
require_relative 'record'
|
6
|
+
|
7
|
+
module Pile
|
8
|
+
# Header that contains the names of each column, used to refer to values of
|
9
|
+
# records by name.
|
10
|
+
#
|
11
|
+
# For example, given a CSV file containing the following header followed by
|
12
|
+
# multiple lines each containing a record,
|
13
|
+
#
|
14
|
+
# "ID,Name,Address Line"
|
15
|
+
#
|
16
|
+
# The parsed array from this row can be passed to +initialize+ after the
|
17
|
+
# +aliases+ hash, which can look like this, assuming +case_sensitive+ is
|
18
|
+
# false:
|
19
|
+
#
|
20
|
+
# \{'id' => ['identity', '#'], 'address line' => ['address']\}
|
21
|
+
class Header
|
22
|
+
# Construct a 'Header' from a CSV-formatted line.
|
23
|
+
def self.from_csv_row row, aliases = {}
|
24
|
+
self.new aliases, *row.parse_csv(converters: [:integer])
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [Hash<String, Array<String>>] aliases A hash of aliases from the column
|
28
|
+
# name to an array of names that contains aliases, each of which can be
|
29
|
+
# used to identify the same column. Without case sensitivity, each key
|
30
|
+
# in this hash is downcased.
|
31
|
+
attr_writer :aliases
|
32
|
+
def aliases= aliases
|
33
|
+
@aliases_downcased = nil
|
34
|
+
|
35
|
+
# Ensure each value is an array; create a singleton array for each one
|
36
|
+
# that isn't.
|
37
|
+
@aliases = {}
|
38
|
+
aliases.each_pair {|k, v| @aliases[k] = v.kind_of?(Array) ? v : [v]}
|
39
|
+
@aliases = aliases
|
40
|
+
end
|
41
|
+
def aliases
|
42
|
+
if case_sensitive
|
43
|
+
@aliases
|
44
|
+
elsif @aliases_downcased
|
45
|
+
@aliases_downcased
|
46
|
+
else
|
47
|
+
downcased = {}
|
48
|
+
@aliases.each_pair {|k, v| downcased[k.downcase] = v}
|
49
|
+
@aliases_downcased = downcased
|
50
|
+
end
|
51
|
+
end
|
52
|
+
# @param [Array<String>] indices The name of each value. Conventionally the
|
53
|
+
# first row in a CSV file.
|
54
|
+
attr_accessor :indices
|
55
|
+
|
56
|
+
# @return [Boolean] Whether indices are case sensitive; defaults to +false+.
|
57
|
+
def case_sensitive
|
58
|
+
@case_sensitive.nil? ? @case_sensitive = false : @case_sensitive
|
59
|
+
end
|
60
|
+
attr_writer :case_sensitive
|
61
|
+
|
62
|
+
# @return [CSV] (nil) Optional CSV object associated with this header; used
|
63
|
+
# for utility functions such as +write_header+.
|
64
|
+
attr_accessor :csv
|
65
|
+
|
66
|
+
#
|
67
|
+
# @param [Hash<String, Array<String>>] aliases A hash of aliases from the column
|
68
|
+
# name to an array of names that contains aliases, each of which can be
|
69
|
+
# used to identify the same column.
|
70
|
+
# @param [Array<String>] indices The name of each value.
|
71
|
+
def initialize(aliases, *indices)
|
72
|
+
@aliases = aliases
|
73
|
+
@indices = indices
|
74
|
+
end
|
75
|
+
|
76
|
+
# Return the integer position that +i+ refers to. This takes into
|
77
|
+
# account the name of each column and the alias hash.
|
78
|
+
def column_index i
|
79
|
+
if case_sensitive
|
80
|
+
position = indices.find_index {|column| column == i || (@aliases.has_key?(column) && @aliases[column].member?(i))}
|
81
|
+
else
|
82
|
+
position = indices.find_index {|column| column.downcase == i.to_s.downcase || (aliases.has_key?(column.downcase) && aliases[column.downcase].any? {|the_alias| the_alias.downcase == i.to_s.downcase})}
|
83
|
+
end
|
84
|
+
|
85
|
+
position ||= (i.kind_of?(Fixnum) ? i : nil)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Write this header to the header's CSV object, if present.
|
89
|
+
#
|
90
|
+
# @param [CSV] csv (nil) If present, the header will be written to the
|
91
|
+
# passed CSV object rather than the header's.
|
92
|
+
def write_header csv = nil
|
93
|
+
csv << indices
|
94
|
+
end
|
95
|
+
|
96
|
+
def ==(other)
|
97
|
+
self.aliases == other.aliases && self.indices == other.indices && self.csv == other.csv
|
98
|
+
end
|
99
|
+
|
100
|
+
def eql?(other)
|
101
|
+
self.aliases.eql?(other.aliases) && self.indices.eql?(other.indices) && self.csv.eql?(other.csv)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Enumerate the record after converting to an array with +to_a+.
|
105
|
+
def each
|
106
|
+
to_a.each
|
107
|
+
end
|
108
|
+
|
109
|
+
# Enumerate each column header.
|
110
|
+
def to_a
|
111
|
+
indices
|
112
|
+
end
|
113
|
+
|
114
|
+
include Enumerable
|
115
|
+
end
|
116
|
+
end
|
data/lib/pile/list.rb
ADDED
@@ -0,0 +1,199 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
require 'matrix'
|
5
|
+
|
6
|
+
require_relative 'header'
|
7
|
+
require_relative 'record'
|
8
|
+
|
9
|
+
module Pile
|
10
|
+
# A database of +Record+s, as an array of records coupled with their header.
|
11
|
+
class List
|
12
|
+
# Construct a +List+ from the contents of a CSV-formatted file.
|
13
|
+
#
|
14
|
+
# @param [String, Array<String>] contents The contents of a file, or an
|
15
|
+
# array of the lines of the file.
|
16
|
+
# @param [Hash<String, Array<String>>] aliases ({})
|
17
|
+
def self.from_string contents, aliases = {}
|
18
|
+
lines = contents.kind_of?(Array) ? contents : contents.lines
|
19
|
+
header = Header.from_csv_row lines[0], aliases
|
20
|
+
self.new header, lines[1..-1].map{|l| Record.from_csv_row l, header}
|
21
|
+
end
|
22
|
+
|
23
|
+
# Construct a +List+ from a matrix. Inverse of +render_rows+.
|
24
|
+
#
|
25
|
+
# @param [Array<Array<String>>, Matrix<String>] matrix
|
26
|
+
def self.from_matrix matrix, aliases = {}
|
27
|
+
matrix = matrix.to_a
|
28
|
+
header = Header.new aliases, *(matrix[0] || [])
|
29
|
+
if matrix.length <= 1
|
30
|
+
# Header only.
|
31
|
+
self.new header, []
|
32
|
+
else
|
33
|
+
self.new header, matrix[1..-1].map{|row| Record.new header, *row}
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Read in filepath_from and write to filepath_to, which must refer to a
|
38
|
+
# different file, yielding to the block given with the header as the first
|
39
|
+
# argument and the record as the second; the block should return a new
|
40
|
+
# +Record+.
|
41
|
+
#
|
42
|
+
# The +Header+ must be the same for each +Record+, but it can be changed if
|
43
|
+
# the same one is returned for each record.
|
44
|
+
#
|
45
|
+
# No value is returned from this method.
|
46
|
+
def self.map_csv_file filepath_from, filepath_to, aliases
|
47
|
+
return to_enum :map_csv_file, filepath_from, filepath_to, aliases unless block_given?
|
48
|
+
|
49
|
+
# write
|
50
|
+
CSV.open(filepath_to, 'wb') do |csv|
|
51
|
+
header = nil
|
52
|
+
empty = true
|
53
|
+
|
54
|
+
# read
|
55
|
+
CSV.foreach(filepath_from, converters: [:integer]) do |row|
|
56
|
+
if !header
|
57
|
+
# Header.
|
58
|
+
header = Header.new aliases, *row
|
59
|
+
else
|
60
|
+
# Record.
|
61
|
+
record = Record.new header, *row
|
62
|
+
record = yield header, record
|
63
|
+
|
64
|
+
if empty
|
65
|
+
empty = false
|
66
|
+
record.write_header csv
|
67
|
+
end
|
68
|
+
|
69
|
+
record.write_record csv
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
# We wait until we check for the first record before writing the header
|
74
|
+
# in case it changed. Write the original one if there are no records.
|
75
|
+
if empty
|
76
|
+
empty = false
|
77
|
+
record.write_header csv
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Like +map_csv_file+, but operates on strings instead of files.
|
83
|
+
def self.map_csv_contents contents, aliases
|
84
|
+
return to_enum :map_csv_contents, contents, aliases unless block_given?
|
85
|
+
|
86
|
+
# write
|
87
|
+
s = CSV.generate do |csv|
|
88
|
+
header = nil
|
89
|
+
empty = true
|
90
|
+
|
91
|
+
# read
|
92
|
+
CSV.parse contents, converters: [:integer] do |row|
|
93
|
+
if !header
|
94
|
+
# Header.
|
95
|
+
header = Header.new aliases, *row
|
96
|
+
else
|
97
|
+
# Record.
|
98
|
+
record = Record.new header, *row
|
99
|
+
record = yield header, record
|
100
|
+
|
101
|
+
if empty
|
102
|
+
empty = false
|
103
|
+
record.write_header csv
|
104
|
+
end
|
105
|
+
|
106
|
+
record.write_record csv
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
# We wait until we check for the first record before writing the header
|
111
|
+
# in case it changed. Write the original one if there are no records.
|
112
|
+
if empty
|
113
|
+
empty = false
|
114
|
+
record.write_header csv
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
attr_accessor :header
|
120
|
+
attr_accessor :records
|
121
|
+
|
122
|
+
# @param [Pile::Header] header
|
123
|
+
# @param [Array<Pile::Record>] records
|
124
|
+
def initialize(header, records)
|
125
|
+
@header = header
|
126
|
+
@records = records
|
127
|
+
end
|
128
|
+
|
129
|
+
# Map each record.
|
130
|
+
#
|
131
|
+
# The +Header+ must be the same for each +Record+, but, unlike +map+, it
|
132
|
+
# can be changed if the same one is returned for each record.
|
133
|
+
def map_records &block
|
134
|
+
records.map &block
|
135
|
+
header = records[0].header unless records.empty?
|
136
|
+
end
|
137
|
+
|
138
|
+
# Generate a CVS-formatted string encoding this list that can be written to
|
139
|
+
# a file.
|
140
|
+
#
|
141
|
+
# @return [String]
|
142
|
+
def csv_string
|
143
|
+
CSV.generate do |csv|
|
144
|
+
header.write_header csv
|
145
|
+
records.each {|r| r.write_record csv}
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
# Return an unwrapped matrix containing the header and each record.
|
150
|
+
#
|
151
|
+
# @return [Array<Array<String>>]
|
152
|
+
def render_rows
|
153
|
+
[header.indices, *(records.map &:values)]
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns a matrix containing the header and each record.
|
157
|
+
#
|
158
|
+
# @return [Matrix<String>]
|
159
|
+
def render_matrix
|
160
|
+
Matrix.rows render_rows
|
161
|
+
end
|
162
|
+
|
163
|
+
def ==(other)
|
164
|
+
self.header == other.header && self.records == other.records
|
165
|
+
end
|
166
|
+
|
167
|
+
def eql?(other)
|
168
|
+
self.header.eql?(other.aliases) && self.records.eql?(other.records)
|
169
|
+
end
|
170
|
+
|
171
|
+
# Enumerate the list records after converting to an array with +to_a+.
|
172
|
+
def each
|
173
|
+
to_a.each
|
174
|
+
end
|
175
|
+
|
176
|
+
# Enumerate each record. Note that the header is not returned.
|
177
|
+
def to_a
|
178
|
+
records
|
179
|
+
end
|
180
|
+
|
181
|
+
# Send everything that the +header+ object recognized to it. Can be used
|
182
|
+
# for +column_index+, etc.
|
183
|
+
def method_missing method, *args, &block
|
184
|
+
header.send method, *args, &block
|
185
|
+
end
|
186
|
+
|
187
|
+
include Enumerable
|
188
|
+
|
189
|
+
# When used as an array, operates on the records.
|
190
|
+
def [](i)
|
191
|
+
records[i]
|
192
|
+
end
|
193
|
+
|
194
|
+
# When used as an array, operates on the records.
|
195
|
+
def []=(i, v)
|
196
|
+
records[i] = v
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
data/lib/pile/record.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
module Pile
|
6
|
+
# Individual record in list of contributors, as an array of values coupled
|
7
|
+
# with its header.
|
8
|
+
#
|
9
|
+
# @param [Pile::Header] header The header defining the structure of
|
10
|
+
# each record; used to determine the type of each entry in the record
|
11
|
+
# by its position.
|
12
|
+
class Record
|
13
|
+
# Construct a 'Header' from a CSV-formatted line.
|
14
|
+
def self.from_csv_row row, header
|
15
|
+
self.new header, *row.parse_csv(converters: [:integer])
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Pile::Header] The header associated with this record.
|
19
|
+
attr_accessor :header
|
20
|
+
# @return [Array<Object>] The values associated with this record. See
|
21
|
+
# below for helper methods that operate on a record's values.
|
22
|
+
attr_accessor :values
|
23
|
+
# @return [CSV] An optional CSV object, which some helper methods use; e.g.
|
24
|
+
# see +add_record_to_csv+
|
25
|
+
attr_accessor :csv
|
26
|
+
|
27
|
+
# @param [Pile::Header] header The header associated with this record,
|
28
|
+
# defining the structure of the record, and by what names (e.g. 'id' and
|
29
|
+
# 'name') values can be indexed.
|
30
|
+
# @param [Array<Object>] values The values in the row of the record.
|
31
|
+
def initialize header, *values
|
32
|
+
@header = header
|
33
|
+
@values = values
|
34
|
+
end
|
35
|
+
|
36
|
+
# Send everything that the +header+ object recognized to it. Can be used
|
37
|
+
# for +column_index+, etc.
|
38
|
+
def method_missing method, *args, &block
|
39
|
+
header.send method, *args, &block
|
40
|
+
end
|
41
|
+
|
42
|
+
# Retrieve a value in the record by its position, or by the column name.
|
43
|
+
# Aliases are recognized.
|
44
|
+
def [](i)
|
45
|
+
values[column_index i]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Set a value in the record by its position, or by the column name.
|
49
|
+
# Aliases are recognized.
|
50
|
+
def []=(i, v)
|
51
|
+
values[column_index i] = v
|
52
|
+
end
|
53
|
+
|
54
|
+
# Write this record to its CSV object, if present.
|
55
|
+
#
|
56
|
+
# @param [CSV] csv (nil) If present, the values will be written to the
|
57
|
+
# passed CSV object rather than the header's.
|
58
|
+
def write_record csv = nil
|
59
|
+
csv ||= self.csv
|
60
|
+
raise 'Record#add_record_to_csv: no associated CSV object.' unless csv
|
61
|
+
|
62
|
+
csv << values
|
63
|
+
end
|
64
|
+
|
65
|
+
def ==(other)
|
66
|
+
self.header == other.header && self.values == other.values && self.csv == other.csv
|
67
|
+
end
|
68
|
+
|
69
|
+
def eql?(other)
|
70
|
+
self.header.eql?(other.header) && self.values.eql?(other.values) && self.csv.eql?(other.csv)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Enumerate the record after converting to an array with +to_a+.
|
74
|
+
def each
|
75
|
+
to_a.each
|
76
|
+
end
|
77
|
+
|
78
|
+
# Enumerate each value.
|
79
|
+
def to_a
|
80
|
+
values
|
81
|
+
end
|
82
|
+
|
83
|
+
include Enumerable
|
84
|
+
end
|
85
|
+
end
|
data/lib/pile/version.rb
ADDED
data/spec/header_spec.rb
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
require_relative '../lib/pile/header.rb'
|
6
|
+
include Pile
|
7
|
+
|
8
|
+
require_relative 'spec_helper'
|
9
|
+
|
10
|
+
describe Header, 'column_index' do
|
11
|
+
include Pile::Helpers
|
12
|
+
|
13
|
+
it 'should return the same integer indices' do
|
14
|
+
header = new_example_header
|
15
|
+
|
16
|
+
header.column_index(2).should == 2
|
17
|
+
header.column_index(0).should == 0
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'should recognized column names' do
|
21
|
+
header = new_example_header
|
22
|
+
|
23
|
+
header.column_index('name').should == 1
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'should recognize aliases' do
|
27
|
+
header = new_example_header
|
28
|
+
|
29
|
+
header.column_index('id').should == 0
|
30
|
+
header.column_index('identity').should == 0
|
31
|
+
|
32
|
+
header.column_index('address line').should == 2
|
33
|
+
header.column_index('address').should == 2
|
34
|
+
end
|
35
|
+
|
36
|
+
it 'should respect case sensitivity' do
|
37
|
+
header = new_example_header
|
38
|
+
|
39
|
+
header.column_index('address line').should == 2
|
40
|
+
header.case_sensitive = true
|
41
|
+
header.column_index('address line').should == nil
|
42
|
+
header.column_index('Address Line').should == 2
|
43
|
+
header.case_sensitive = false
|
44
|
+
header.column_index('address line').should == 2
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe Header, 'write_header' do
|
49
|
+
include Pile::Helpers
|
50
|
+
|
51
|
+
it 'writes the example header that matches our string' do
|
52
|
+
header = new_example_header
|
53
|
+
|
54
|
+
read_write_tempfile 'csv-spec' do |file, step|
|
55
|
+
case step
|
56
|
+
when :write
|
57
|
+
file.write (CSV.generate {|csv| header.write_header csv})
|
58
|
+
when :read
|
59
|
+
file.read.should == "ID,Name,Address Line\n"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'is the right-inverse of from_csv_row' do
|
65
|
+
header = new_example_header
|
66
|
+
|
67
|
+
read_write_tempfile 'csv-spec' do |file, step|
|
68
|
+
case step
|
69
|
+
when :write
|
70
|
+
file.write (CSV.generate {|csv| header.write_header csv})
|
71
|
+
when :read
|
72
|
+
header2 = Header.from_csv_row file.read, header.aliases
|
73
|
+
header2.should == header
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe Header, '==' do
|
80
|
+
include Pile::Helpers
|
81
|
+
|
82
|
+
it 'should not consider headers with different indices the same' do
|
83
|
+
header = new_example_header
|
84
|
+
header2 = new_example_header
|
85
|
+
header2.indices[3] = 'Country'
|
86
|
+
|
87
|
+
header2.should_not == header
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'should not consider headers with different indices the same' do
|
91
|
+
header = new_example_header
|
92
|
+
header2 = new_example_header
|
93
|
+
header2.aliases['name'] = ['handle', 'nick']
|
94
|
+
|
95
|
+
header2.should_not == header
|
96
|
+
end
|
97
|
+
|
98
|
+
it 'should consider headers with the same indices and aliases as equal' do
|
99
|
+
header = new_example_header
|
100
|
+
header2 = new_example_header
|
101
|
+
|
102
|
+
header2.should == header
|
103
|
+
end
|
104
|
+
end
|
data/spec/list_spec.rb
ADDED
@@ -0,0 +1,92 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require_relative '../lib/pile/list.rb'
|
4
|
+
include Pile
|
5
|
+
|
6
|
+
require_relative 'spec_helper'
|
7
|
+
|
8
|
+
describe List, '::map_csv_contents' do
|
9
|
+
include Pile::Helpers
|
10
|
+
|
11
|
+
it 'responds to mappings as we expect' do
|
12
|
+
file_contents = "ID,Name,Address Line\n1,Alice,123 1st St\n2,Bob,234 2nd St\n3,Charles,345 3rd St\n"
|
13
|
+
aliases = {'Address Line' => ['address']}
|
14
|
+
|
15
|
+
updated_contents = List.map_csv_contents file_contents, aliases do |header, record|
|
16
|
+
record['id'] += 1
|
17
|
+
record
|
18
|
+
end
|
19
|
+
|
20
|
+
updated_contents.should == "ID,Name,Address Line\n2,Alice,123 1st St\n3,Bob,234 2nd St\n4,Charles,345 3rd St\n"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
describe List, '::csv_string' do
|
25
|
+
it 'returns the contents we expect' do
|
26
|
+
file_contents = example_list_csv_string
|
27
|
+
|
28
|
+
list = List.from_string file_contents
|
29
|
+
output = list.csv_string
|
30
|
+
|
31
|
+
list.csv_string.should == "ID,Name,Address Line\n1,Alice,123 1st St\n2,Bob,234 2nd St\n3,Charles,345 3rd St\n"
|
32
|
+
end
|
33
|
+
|
34
|
+
it 'is the right-inverse of from_string' do
|
35
|
+
list = List.from_string example_list_csv_string
|
36
|
+
|
37
|
+
read_write_tempfile 'csv-list-spec' do |file, step|
|
38
|
+
case step
|
39
|
+
when :write
|
40
|
+
file.write list.csv_string
|
41
|
+
when :read
|
42
|
+
list2 = List.from_string file.read
|
43
|
+
|
44
|
+
list2.should == list
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe List, '::render_rows' do
|
51
|
+
it 'returns the output we expect' do
|
52
|
+
file_contents = example_list_csv_string
|
53
|
+
|
54
|
+
list = List.from_string file_contents
|
55
|
+
output = list.render_rows
|
56
|
+
|
57
|
+
output.should == [["ID", "Name", "Address Line"], [1, "Alice", "123 1st St"], [2, "Bob", "234 2nd St"], [3, "Charles", "345 3rd St"]]
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'is the right-inverse of from_matrix' do
|
61
|
+
list = List.from_string example_list_csv_string
|
62
|
+
list2 = List.from_matrix list.render_rows
|
63
|
+
|
64
|
+
list2.should == list
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe List, '::render_matrix' do
|
69
|
+
it 'is the right-inverse of from_matrix' do
|
70
|
+
list = List.from_string example_list_csv_string
|
71
|
+
list2 = List.from_matrix list.render_matrix
|
72
|
+
|
73
|
+
list2.should == list
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe List, '==' do
|
78
|
+
it 'should not consider lists with different headers the same' do
|
79
|
+
list = new_example_list
|
80
|
+
list2 = new_example_list
|
81
|
+
list2.header.indices[0] = 'ID#'
|
82
|
+
|
83
|
+
list2.should_not == list
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'should consider lists with the same headers and records as equal' do
|
87
|
+
list = new_example_list
|
88
|
+
list2 = new_example_list
|
89
|
+
|
90
|
+
list2.should == list
|
91
|
+
end
|
92
|
+
end
|
data/spec/record_spec.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
require_relative '../lib/pile/record.rb'
|
6
|
+
include Pile
|
7
|
+
include Helpers
|
8
|
+
|
9
|
+
require_relative 'spec_helper'
|
10
|
+
|
11
|
+
describe Record, '[]' do
|
12
|
+
it 'should return the appropriate values' do
|
13
|
+
record = new_example_record
|
14
|
+
|
15
|
+
record[1].should == 'Bob Smith'
|
16
|
+
record['name'].should == 'Bob Smith'
|
17
|
+
record['address'].should == '123 1st St'
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe Record, '[]=' do
|
22
|
+
it 'should update values' do
|
23
|
+
record = new_example_record
|
24
|
+
|
25
|
+
record[1].should == 'Bob Smith'
|
26
|
+
record['name'].should == 'Bob Smith'
|
27
|
+
record['address'].should == '123 1st St'
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe Record, 'write_record' do
|
32
|
+
it 'writes the example record that matches our string' do
|
33
|
+
record = new_example_record
|
34
|
+
|
35
|
+
read_write_tempfile 'csv-record-spec' do |file, step|
|
36
|
+
case step
|
37
|
+
when :write
|
38
|
+
contents = CSV.generate do |csv|
|
39
|
+
record.write_header csv
|
40
|
+
record.write_record csv
|
41
|
+
end
|
42
|
+
file.write contents
|
43
|
+
when :read
|
44
|
+
file.read.should == "ID,Name,Address Line\n3,Bob Smith,123 1st St\n"
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'is the right-inverse of from_csv_row' do
|
50
|
+
record = new_example_record
|
51
|
+
|
52
|
+
read_write_tempfile 'csv-record-spec' do |file, step|
|
53
|
+
case step
|
54
|
+
when :write
|
55
|
+
contents = CSV.generate do |csv|
|
56
|
+
record.write_header csv
|
57
|
+
record.write_record csv
|
58
|
+
end
|
59
|
+
file.write contents
|
60
|
+
when :read
|
61
|
+
lines = file.readlines
|
62
|
+
|
63
|
+
header2 = Header.from_csv_row lines[0], record.aliases
|
64
|
+
record2 = Record.from_csv_row lines[1], header2
|
65
|
+
|
66
|
+
record2.should == record
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe Record, '==' do
|
73
|
+
it 'should not consider records with different values the same' do
|
74
|
+
record = new_example_record
|
75
|
+
record2 = new_example_record
|
76
|
+
record2[1] = 'Bob Johnson'
|
77
|
+
|
78
|
+
record2.should_not == record
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'should consider records with the same indices and aliases as equal' do
|
82
|
+
record = new_example_record
|
83
|
+
record2 = new_example_record
|
84
|
+
|
85
|
+
record2.should == record
|
86
|
+
end
|
87
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,48 @@
|
|
1
|
+
module Pile
|
2
|
+
module Helpers
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
# Construct a new header as specified in the example in the documentation
|
6
|
+
# of the +Header+ class.
|
7
|
+
def new_example_header
|
8
|
+
header = Header.new ({'id' => ['identity', '#'], 'address line' => ['address']}),
|
9
|
+
'ID', 'Name', 'Address Line'
|
10
|
+
end
|
11
|
+
|
12
|
+
# Open and close a +Tempfile+ around a block yielded to with the +Tempfile+
|
13
|
+
# object.
|
14
|
+
def with_tempfile name
|
15
|
+
file = Tempfile.new name
|
16
|
+
begin
|
17
|
+
yield file
|
18
|
+
ensure
|
19
|
+
file.close
|
20
|
+
file.unlink
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
# Calls the block twice: first with +:write+ as the second argument, and
|
25
|
+
# then second with +:read+ as the second argument. The file is rewound to
|
26
|
+
# the beginning in between.
|
27
|
+
def read_write_tempfile name
|
28
|
+
with_tempfile name do |file|
|
29
|
+
yield file, :write
|
30
|
+
file.rewind
|
31
|
+
yield file, :read
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def new_example_record
|
36
|
+
Record.new new_example_header, 3, 'Bob Smith', '123 1st St'
|
37
|
+
end
|
38
|
+
|
39
|
+
def new_example_list
|
40
|
+
header = new_example_header
|
41
|
+
List.new header, [Record.new(header), *new_example_record.values]
|
42
|
+
end
|
43
|
+
|
44
|
+
def example_list_csv_string
|
45
|
+
"ID,Name,Address Line\n1,Alice,123 1st St\n2,Bob,234 2nd St\n3,Charles,345 3rd St\n"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
metadata
ADDED
@@ -0,0 +1,74 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: pile
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Byron Johnson
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-08-13 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rspec
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - '>='
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - '>='
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: pile provides classes for updating, reading, and writing CSV files that
|
28
|
+
consist of a header and a number of records.
|
29
|
+
email:
|
30
|
+
- byron@byronjohnson.net
|
31
|
+
executables: []
|
32
|
+
extensions: []
|
33
|
+
extra_rdoc_files: []
|
34
|
+
files:
|
35
|
+
- Rakefile
|
36
|
+
- lib/pile/version.rb
|
37
|
+
- lib/pile/record.rb
|
38
|
+
- lib/pile/list.rb
|
39
|
+
- lib/pile/header.rb
|
40
|
+
- lib/pile.rb
|
41
|
+
- spec/spec_helper.rb
|
42
|
+
- spec/record_spec.rb
|
43
|
+
- spec/list_spec.rb
|
44
|
+
- spec/header_spec.rb
|
45
|
+
homepage: https://github.com/bairyn/pile
|
46
|
+
licenses:
|
47
|
+
- BSD3
|
48
|
+
metadata: {}
|
49
|
+
post_install_message:
|
50
|
+
rdoc_options: []
|
51
|
+
require_paths:
|
52
|
+
- lib
|
53
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
54
|
+
requirements:
|
55
|
+
- - '>='
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '0'
|
58
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
requirements: []
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 2.0.6
|
66
|
+
signing_key:
|
67
|
+
specification_version: 4
|
68
|
+
summary: CSV file manipulation library.
|
69
|
+
test_files:
|
70
|
+
- spec/spec_helper.rb
|
71
|
+
- spec/record_spec.rb
|
72
|
+
- spec/list_spec.rb
|
73
|
+
- spec/header_spec.rb
|
74
|
+
has_rdoc:
|