pile 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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:
|