csv_madness 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +14 -0
- data/LICENSE.txt +20 -0
- data/README.rdoc +174 -0
- data/Rakefile +68 -0
- data/VERSION +1 -0
- data/lib/csv_madness.rb +14 -0
- data/lib/csv_madness/record.rb +43 -0
- data/lib/csv_madness/sheet.rb +378 -0
- data/test/csv/simple.csv +5 -0
- data/test/helper.rb +21 -0
- data/test/test_csv_madness.rb +232 -0
- metadata +143 -0
data/Gemfile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
# Add dependencies required to use your gem here.
|
3
|
+
# Example:
|
4
|
+
# gem "activesupport", ">= 2.3.5"
|
5
|
+
|
6
|
+
# Add dependencies to develop your gem here.
|
7
|
+
# Include everything needed to run rake, tests, features, etc.
|
8
|
+
group :development do
|
9
|
+
gem "shoulda", ">= 0"
|
10
|
+
gem "rdoc", "~> 3.12"
|
11
|
+
gem "bundler", "~> 1.3.0"
|
12
|
+
gem "jeweler", "~> 1.8.4"
|
13
|
+
gem "debugger"
|
14
|
+
end
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Bryce Anderson
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,174 @@
|
|
1
|
+
= csv_madness : turn your CSV rows into happycrazy objects.
|
2
|
+
|
3
|
+
== What is it?
|
4
|
+
|
5
|
+
CSV Madness tries to remove what little pain is left from Ruby's CSV class. Load a CSV file, and manipulate your data using an array of objects with customizable getter/setter methods.
|
6
|
+
|
7
|
+
|
8
|
+
== Why should I use it?
|
9
|
+
|
10
|
+
I like brief code and I cannot lie.
|
11
|
+
|
12
|
+
|
13
|
+
== Examples
|
14
|
+
|
15
|
+
CsvMadness makes some assumptions about your CSV file. It does assume headers, for example.
|
16
|
+
|
17
|
+
The simplest case is when your columns are nicely named. For example, if you have a csv file named <tt>~/data/people.csv</tt>:
|
18
|
+
|
19
|
+
<code>
|
20
|
+
"id","fname","lname","age","born"
|
21
|
+
"1","Mary","Moore","27","1986-04-08 15:06:10"
|
22
|
+
"2","Bill","Paxton","39","1974-02-22"
|
23
|
+
"3","Charles","Darwin","72",""
|
24
|
+
"4","Chuck","Norris","57","1901-03-02"
|
25
|
+
</code>
|
26
|
+
|
27
|
+
... then you can write code like so:
|
28
|
+
|
29
|
+
'''require 'csv_madness'
|
30
|
+
sheet = CsvMadness.load( "~/data/people.csv" )
|
31
|
+
sheet.columns # => [:id, :fname, :lname, :age, :born]
|
32
|
+
sheet.records.map(&:id) # => ["1", "2", "3", "4"]
|
33
|
+
sheet.set_column_type(:id, :float)
|
34
|
+
sheet.records.map(&:id) # => [1.0, 2.0, 3.0, 4.0]
|
35
|
+
sheet.alter_column(:id) do |id|
|
36
|
+
id + rand() - 0.5
|
37
|
+
end
|
38
|
+
|
39
|
+
sheet.records.map(&:id) # => [0.7186, 2.30134, 2.90132, 4.30124] (your results may vary)
|
40
|
+
mary = sheet[0]
|
41
|
+
|
42
|
+
"#{mary.lname}, #{mary.fname} (#{mary.id})" # => "Moore, Mary (1)"
|
43
|
+
'''
|
44
|
+
|
45
|
+
If you're not satisfied with your column names, you can send your own, in the column order. An index can also be provided, which will allow you to use <tt>.fetch()</tt> to find specific records quickly:
|
46
|
+
|
47
|
+
'''require 'csv_madness'
|
48
|
+
sheet = CsvMadness.load( "~/data/people.csv",
|
49
|
+
columns: [:uid, :first_name, :last_name, :years_on_planet, :birthday],
|
50
|
+
index: :uid )
|
51
|
+
|
52
|
+
sheet.fetch("2").years_on_planet # => "39"
|
53
|
+
'''
|
54
|
+
|
55
|
+
**Note:** you can provide multiple indexes as an array. However many columns you index, you'll run into trouble if the index isn't unique for each record. (that may change in the future)
|
56
|
+
|
57
|
+
|
58
|
+
It's useful to clean up your files. Say you have:
|
59
|
+
|
60
|
+
""""id","fname","lname","age"," born "
|
61
|
+
"1 ","Mary ","Moore","27","1986-04-08 15:06:10"
|
62
|
+
""," Bill ","Paxton",," Feb. 22, 1974 "
|
63
|
+
,"Charles "," Darwin","72 ",
|
64
|
+
"4","Chuck","Norris",," 2 March 1901 "
|
65
|
+
"""
|
66
|
+
|
67
|
+
Ick. Missing IDs, inconsistent date formats, leading and trailing whitespace... We can fix this.
|
68
|
+
|
69
|
+
"""require 'csv_madness'
|
70
|
+
sheet = CsvMadness.load( "~/data/people.csv" )
|
71
|
+
sheet.alter_cells do |cell, record|
|
72
|
+
(cell || "").strip
|
73
|
+
end # removes leading and trailing whitespace, and turns nils into ""
|
74
|
+
|
75
|
+
sheet.set_column_type(:id, :integer, nil) # the last argument provides a default for blank records.
|
76
|
+
|
77
|
+
# assumes the missing ids can be filled in sequentially.
|
78
|
+
# While .alter_column() does take a blank as a second argument,
|
79
|
+
# that's not what we want here. If a blank is provided, your
|
80
|
+
# code will never see the records with blank entries.
|
81
|
+
sheet.alter_column(:id) do |id, record|
|
82
|
+
@count ||= 1
|
83
|
+
if id.nil?
|
84
|
+
id = @count
|
85
|
+
@count += 1
|
86
|
+
else
|
87
|
+
@count = id + 1
|
88
|
+
end
|
89
|
+
|
90
|
+
id
|
91
|
+
end
|
92
|
+
|
93
|
+
sheet.records.map(&:id) # => [1, 2, 3, 4]
|
94
|
+
|
95
|
+
require 'time'
|
96
|
+
sheet.alter_column(:born) do |date_string|
|
97
|
+
begin
|
98
|
+
Time.parse( date_string ).strftime("%Y-%m-%d")
|
99
|
+
rescue ArgumentError
|
100
|
+
""
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
sheet.records.map(&:date) # => ["1986-04-08", "1974-02-22", "", "1901-03-02"]
|
105
|
+
|
106
|
+
# The same thing can be accomplished more simply by saying <tt>sheet.set_column_type(:date)</tt>.
|
107
|
+
# Even better, record.date is then a Time object
|
108
|
+
|
109
|
+
# Now calculate everyone's age (ignoring and overriding the existing data)
|
110
|
+
sheet.alter_column(:age) do |age, record|
|
111
|
+
if record.blank?(:born)
|
112
|
+
""
|
113
|
+
else
|
114
|
+
((Time.now - Time.parse(record.born)) / 365.25 / 24 / 60 / 60).to_i # not production-worthy code
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
sheet.write_to_file( "~/data/people.clean.csv", force_quotes: true ) # save the cleaned data for the next stage of your process
|
119
|
+
"""
|
120
|
+
|
121
|
+
You could do something similar to clean and standardize phone numbers, detect and delete/complete invalid emails, etc.
|
122
|
+
|
123
|
+
|
124
|
+
=== Adding and removing columns
|
125
|
+
|
126
|
+
# Add 72 years to the date born.
|
127
|
+
sheet.set_column_type( :born, :date ) # replace date strings with Time objects
|
128
|
+
|
129
|
+
sheet.add_column( :expected_death_date ) do |date, record|
|
130
|
+
record.born + ( 72 * 365 * 24 * 60 * 60 )
|
131
|
+
end
|
132
|
+
|
133
|
+
puts sheet[0].expected_death_date # should be in 2058
|
134
|
+
|
135
|
+
# But that's just morbid, so we drop the column
|
136
|
+
sheet.drop_column( :expected_death_date )
|
137
|
+
|
138
|
+
|
139
|
+
=== Using columns
|
140
|
+
|
141
|
+
sheet.set_column_type( :id, :integer )
|
142
|
+
|
143
|
+
|
144
|
+
# Returns each record's id, as an array
|
145
|
+
sheet.column( :id ).max # ==> 4
|
146
|
+
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
|
151
|
+
There are lots of other features, but they'll take time to test and document.
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
|
156
|
+
|
157
|
+
|
158
|
+
== Contributing to csv_madness
|
159
|
+
|
160
|
+
Instructions are boilerplate from Jeweler, but they make sense enough:
|
161
|
+
|
162
|
+
* Check out the latest master to make sure the feature hasn't been implemented or the bug hasn't been fixed yet.
|
163
|
+
* Check out the issue tracker to make sure someone already hasn't requested it and/or contributed it.
|
164
|
+
* Fork the project.
|
165
|
+
* Start a feature/bugfix branch.
|
166
|
+
* Commit and push until you are happy with your contribution.
|
167
|
+
* Make sure to add tests for it. This is important so I don't break it in a future version unintentionally.
|
168
|
+
* Please try not to mess with the Rakefile, version, or history. If you want to have your own version, or is otherwise necessary, that is fine, but please isolate to its own commit so I can cherry-pick around it.
|
169
|
+
|
170
|
+
|
171
|
+
== Copyright
|
172
|
+
|
173
|
+
Copyright (c) 2013 Bryce Anderson. See LICENSE.txt for further details.
|
174
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'bundler'
|
5
|
+
|
6
|
+
begin
|
7
|
+
Bundler.setup(:default, :development)
|
8
|
+
rescue Bundler::BundlerError => e
|
9
|
+
$stderr.puts e.message
|
10
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
11
|
+
exit e.status_code
|
12
|
+
end
|
13
|
+
|
14
|
+
require 'rake'
|
15
|
+
|
16
|
+
require 'jeweler'
|
17
|
+
|
18
|
+
Jeweler::Tasks.new do |gem|
|
19
|
+
# gem is a Gem::Specification... see http://docs.rubygems.org/read/chapter/20 for more options
|
20
|
+
gem.name = "csv_madness"
|
21
|
+
gem.homepage = "http://github.com/darthschmoo/csv_madness"
|
22
|
+
gem.license = "MIT"
|
23
|
+
gem.summary = %Q{CSV Madness turns your CSV rows into happycrazy objects.}
|
24
|
+
gem.description = %Q{CSV Madness removes what little pain is left from Ruby's CSV class. Load a CSV file, and get back an array of objects with customizable getter/setter methods.}
|
25
|
+
gem.email = "keeputahweird@gmail.com"
|
26
|
+
gem.authors = ["Bryce Anderson"]
|
27
|
+
# dependencies defined in Gemfile
|
28
|
+
gem.files = [ "./lib/csv_madness/csv_recipe.rb",
|
29
|
+
"./lib/csv_madness/record.rb",
|
30
|
+
"./lib/csv_madness/sheet.rb",
|
31
|
+
"./lib/csv_madness.rb",
|
32
|
+
"./Gemfile",
|
33
|
+
"./VERSION",
|
34
|
+
"./README.rdoc",
|
35
|
+
"./Rakefile",
|
36
|
+
"./test/csv/simple.csv",
|
37
|
+
"./test/helper.rb",
|
38
|
+
"./test/test_csv_madness.rb" ]
|
39
|
+
end
|
40
|
+
|
41
|
+
Jeweler::RubygemsDotOrgTasks.new
|
42
|
+
|
43
|
+
require 'rake/testtask'
|
44
|
+
Rake::TestTask.new(:test) do |test|
|
45
|
+
test.libs << 'lib' << 'test'
|
46
|
+
test.pattern = 'test/**/test_*.rb'
|
47
|
+
test.verbose = true
|
48
|
+
end
|
49
|
+
|
50
|
+
# require 'rcov/rcovtask'
|
51
|
+
# Rcov::RcovTask.new do |test|
|
52
|
+
# test.libs << 'test'
|
53
|
+
# test.pattern = 'test/**/test_*.rb'
|
54
|
+
# test.verbose = true
|
55
|
+
# test.rcov_opts << '--exclude "gems/*"'
|
56
|
+
# end
|
57
|
+
|
58
|
+
task :default => :test
|
59
|
+
|
60
|
+
require 'rdoc/task'
|
61
|
+
Rake::RDocTask.new do |rdoc|
|
62
|
+
version = File.exist?('VERSION') ? File.read('VERSION') : ""
|
63
|
+
|
64
|
+
rdoc.rdoc_dir = 'rdoc'
|
65
|
+
rdoc.title = "csv_madness #{version}"
|
66
|
+
rdoc.rdoc_files.include('README*')
|
67
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
68
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.0.2
|
data/lib/csv_madness.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'pathname'
|
3
|
+
require 'time' # to use Time.parse to parse cells to get the date
|
4
|
+
require 'debugger'
|
5
|
+
|
6
|
+
require_relative 'csv_madness/data_accessor_module'
|
7
|
+
require_relative 'csv_madness/sheet'
|
8
|
+
require_relative 'csv_madness/record'
|
9
|
+
|
10
|
+
CsvMadness.class_eval do
|
11
|
+
def self.load( csv, opts = {} )
|
12
|
+
CsvMadness::Sheet.from( csv, opts )
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module CsvMadness
|
2
|
+
# Every Sheet you instantiate will have its very own,
|
3
|
+
# anonymous subclass of Record class, which will be
|
4
|
+
# extended by the spreadsheet's own getter/setter module.
|
5
|
+
#
|
6
|
+
# @csv_data is currently a CsvRow. That may change in the
|
7
|
+
# future. I'd like to be able to address by row and by
|
8
|
+
# symbol.
|
9
|
+
class Record
|
10
|
+
attr_accessor :csv_data
|
11
|
+
def initialize( data )
|
12
|
+
@csv_data = data
|
13
|
+
end
|
14
|
+
|
15
|
+
def [] key
|
16
|
+
@csv_data[key]
|
17
|
+
end
|
18
|
+
|
19
|
+
def []= key, val
|
20
|
+
@csv_data[key] = val
|
21
|
+
end
|
22
|
+
|
23
|
+
def columns
|
24
|
+
self.class.spreadsheet.columns
|
25
|
+
end
|
26
|
+
|
27
|
+
def self.spreadsheet= sheet
|
28
|
+
@spreadsheet = sheet
|
29
|
+
end
|
30
|
+
|
31
|
+
def self.spreadsheet
|
32
|
+
@spreadsheet
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_csv( opts = {} )
|
36
|
+
self.columns.map{|col| self.send(col) }.to_csv( opts )
|
37
|
+
end
|
38
|
+
|
39
|
+
def blank?( col )
|
40
|
+
(self.send( col ).to_s || "").strip.length == 0
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,378 @@
|
|
1
|
+
module CsvMadness
|
2
|
+
class Sheet
|
3
|
+
COLUMN_TYPES = {
|
4
|
+
number: Proc.new do |cell, record|
|
5
|
+
if (cell || "").strip.match(/^\d*$/)
|
6
|
+
cell.to_i
|
7
|
+
else
|
8
|
+
cell.to_f
|
9
|
+
end
|
10
|
+
end,
|
11
|
+
|
12
|
+
integer: Proc.new do |cell, record|
|
13
|
+
cell.to_i
|
14
|
+
end,
|
15
|
+
|
16
|
+
float: Proc.new do |cell, record|
|
17
|
+
cell.to_f
|
18
|
+
end,
|
19
|
+
|
20
|
+
date: Proc.new do |cell, record|
|
21
|
+
begin
|
22
|
+
parse = Time.parse( cell )
|
23
|
+
rescue ArgumentError
|
24
|
+
parse = "Invalid Time Format: <#{cell}>"
|
25
|
+
end
|
26
|
+
parse
|
27
|
+
end
|
28
|
+
}
|
29
|
+
|
30
|
+
# Used to make getter/setter names out of the original header strings.
|
31
|
+
# " hello;: world! " => :hello_world
|
32
|
+
def self.getter_name( name )
|
33
|
+
name = name.strip.gsub(/\s+/,"_").gsub(/(\W|_)+/, "" ).downcase
|
34
|
+
if name.match( /^\d/ )
|
35
|
+
name = "_#{name}"
|
36
|
+
end
|
37
|
+
|
38
|
+
name.to_sym
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
# Paths to be searched when CsvMadness.load( "filename.csv" ) is called.
|
43
|
+
def self.add_search_path( path )
|
44
|
+
@search_paths ||= []
|
45
|
+
path = Pathname.new( path ).expand_path
|
46
|
+
@search_paths << path unless @search_paths.include?( path )
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.from( csv_file, opts = {} )
|
50
|
+
if f = find_spreadsheet_in_filesystem( csv_file )
|
51
|
+
Sheet.new( f, opts )
|
52
|
+
else
|
53
|
+
raise "File not found."
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Search absolute/relative-to-current-dir before checking
|
58
|
+
# search paths.
|
59
|
+
def self.find_spreadsheet_in_filesystem( name )
|
60
|
+
@search_paths ||= []
|
61
|
+
|
62
|
+
expanded_path = Pathname.new( name ).expand_path
|
63
|
+
if expanded_path.exist?
|
64
|
+
return expanded_path
|
65
|
+
else # look for it in the search paths
|
66
|
+
@search_paths.each do |p|
|
67
|
+
file = p.join( name )
|
68
|
+
if file.exist? && file.file?
|
69
|
+
return p.join( name )
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
nil
|
75
|
+
end
|
76
|
+
|
77
|
+
# opts are passed to underlying CSV (:row_sep, :encoding, :force_quotes)
|
78
|
+
def self.to_csv( spreadsheet, opts = {} )
|
79
|
+
out = spreadsheet.columns.to_csv( opts )
|
80
|
+
spreadsheet.records.inject( out ) do |output, record|
|
81
|
+
output << record.to_csv( opts )
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.write_to_file( spreadsheet, file, opts = {} )
|
86
|
+
file = Pathname.new(file).expand_path
|
87
|
+
File.open( file, "w" ) do |f|
|
88
|
+
f << spreadsheet.to_csv( opts )
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def write_to_file( file, opts = {} )
|
93
|
+
self.class.write_to_file( self, file, opts )
|
94
|
+
end
|
95
|
+
|
96
|
+
def to_csv( opts = {} )
|
97
|
+
self.records.inject( self.columns.to_csv( opts ) ) do |output, record|
|
98
|
+
output << record.to_csv( opts )
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
attr_reader :columns, :records, :spreadsheet_file, :record_class
|
103
|
+
# opts:
|
104
|
+
# index: ( [:id, :id2 ] )
|
105
|
+
# columns you want mapped for quick
|
106
|
+
# lookup of individual records
|
107
|
+
#
|
108
|
+
# columns: ( [:fname, :lname, :age] )
|
109
|
+
# an array of symbols, corresponding
|
110
|
+
# to the csv rows they represent (first, second, third)
|
111
|
+
# and designating the method for calling the cell in
|
112
|
+
# a given record. If not provided, it will guess based
|
113
|
+
# on the header row.
|
114
|
+
#
|
115
|
+
# header: false
|
116
|
+
# anything else, we assume the csv file has a header row
|
117
|
+
def initialize( spreadsheet, opts = {} )
|
118
|
+
@spreadsheet_file = self.class.find_spreadsheet_in_filesystem( spreadsheet )
|
119
|
+
@opts = opts
|
120
|
+
@opts[:header] = (@opts[:header] == false ? false : true) # true unless already explicitly set to false
|
121
|
+
|
122
|
+
load_csv
|
123
|
+
|
124
|
+
set_initial_columns( @opts[:columns] )
|
125
|
+
|
126
|
+
|
127
|
+
create_record_class
|
128
|
+
package
|
129
|
+
|
130
|
+
@index_columns = case @opts[:index]
|
131
|
+
when NilClass
|
132
|
+
[]
|
133
|
+
when Symbol
|
134
|
+
[ @opts[:index] ]
|
135
|
+
when Array
|
136
|
+
@opts[:index]
|
137
|
+
end
|
138
|
+
|
139
|
+
reindex
|
140
|
+
end
|
141
|
+
|
142
|
+
def [] offset
|
143
|
+
@records[offset]
|
144
|
+
end
|
145
|
+
|
146
|
+
# Fetches an indexed record based on the column indexed and the keying object.
|
147
|
+
# If key is an array of keying objects, returns an array of records in the
|
148
|
+
# same order that the keying objects appear.
|
149
|
+
# Index column should yield a different, unique value for each record.
|
150
|
+
def fetch( index_col, key )
|
151
|
+
if key.is_a?(Array)
|
152
|
+
key.map{ |key| @indexes[index_col][key] }
|
153
|
+
else
|
154
|
+
@indexes[index_col][key]
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# function should take an object, and return either true or false
|
159
|
+
# returns an array of objects that respond true when put through the
|
160
|
+
# meat grinder
|
161
|
+
def filter( &block )
|
162
|
+
rval = []
|
163
|
+
@records.each do |record|
|
164
|
+
rval << record if ( yield record )
|
165
|
+
end
|
166
|
+
|
167
|
+
rval
|
168
|
+
end
|
169
|
+
|
170
|
+
# removes rows which fail the given test from the spreadsheet.
|
171
|
+
def filter!( &block )
|
172
|
+
@records = self.filter( &block )
|
173
|
+
reindex
|
174
|
+
@records
|
175
|
+
end
|
176
|
+
|
177
|
+
def column col
|
178
|
+
@records.map(&col)
|
179
|
+
end
|
180
|
+
|
181
|
+
# retrieve multiple columns. Returns an array of the form
|
182
|
+
# [ [record1:col1, record1:col2...], [record2:col1, record2:col2...] [...] ]
|
183
|
+
def multiple_columns(*args)
|
184
|
+
@records.inject([]){ |memo, record|
|
185
|
+
memo << args.map{ |arg| record.send(arg) }
|
186
|
+
memo
|
187
|
+
}
|
188
|
+
end
|
189
|
+
|
190
|
+
# if blank is defined, only the records which are non-blank in that
|
191
|
+
# column will actually be yielded. The rest will be set to the provided
|
192
|
+
# default
|
193
|
+
def alter_cells( blank = :undefined, &block )
|
194
|
+
@columns.each_with_index do |column, cindex|
|
195
|
+
alter_column( column, blank, &block )
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# if column doesn't exist, silently fails. Proper behavior? Dunno.
|
200
|
+
def alter_column( column, blank = :undefined, &block )
|
201
|
+
if cindex = @columns.index( column )
|
202
|
+
for record in @records
|
203
|
+
if record.blank?(column) && blank != :undefined
|
204
|
+
record[cindex] = blank
|
205
|
+
else
|
206
|
+
record[cindex] = yield( record[cindex], record )
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def add_column( column, &block )
|
213
|
+
raise "Column already exists" if @columns.include?( column )
|
214
|
+
@columns << column
|
215
|
+
|
216
|
+
# add empty column to each row
|
217
|
+
@records.map{ |r|
|
218
|
+
r.csv_data << {column => ""}
|
219
|
+
}
|
220
|
+
|
221
|
+
update_data_accessor_module
|
222
|
+
|
223
|
+
if block_given?
|
224
|
+
alter_column( column ) do |val, record|
|
225
|
+
yield val, record
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
|
230
|
+
def drop_column( column )
|
231
|
+
raise "Column does not exist" unless @columns.include?( column )
|
232
|
+
|
233
|
+
@columns.delete( column )
|
234
|
+
|
235
|
+
key = column.to_s
|
236
|
+
|
237
|
+
@records.map{ |r|
|
238
|
+
r.csv_data.delete( key )
|
239
|
+
}
|
240
|
+
|
241
|
+
update_data_accessor_module
|
242
|
+
end
|
243
|
+
|
244
|
+
def set_column_type( column, type, blank = :undefined )
|
245
|
+
alter_column( column, blank, &COLUMN_TYPES[type] )
|
246
|
+
end
|
247
|
+
|
248
|
+
# Note: If a block is given, the mod arg will be ignored.
|
249
|
+
def add_record_methods( mod = nil, &block )
|
250
|
+
if block_given?
|
251
|
+
mod = Module.new( &block )
|
252
|
+
end
|
253
|
+
@record_class.send( :include, mod )
|
254
|
+
self
|
255
|
+
end
|
256
|
+
|
257
|
+
# Note: If implementation of Record[] changes, so must this.
|
258
|
+
def nils_are_blank_strings
|
259
|
+
alter_cells do |value, record|
|
260
|
+
value.nil? ? "" : value
|
261
|
+
end
|
262
|
+
end
|
263
|
+
|
264
|
+
protected
|
265
|
+
def load_csv
|
266
|
+
# encoding seems to solve a specific problem with a specific spreadsheet, at an unknown cost.
|
267
|
+
@csv = CSV.new( File.read(@spreadsheet_file).force_encoding("ISO-8859-1").encode("UTF-8"),
|
268
|
+
{ write_headers: true,
|
269
|
+
headers: ( @opts[:header] ? :first_row : false ) } )
|
270
|
+
end
|
271
|
+
|
272
|
+
def add_to_index( col, key, record )
|
273
|
+
@indexes[col][key] = record
|
274
|
+
end
|
275
|
+
|
276
|
+
# Reindexes the record lookup tables.
|
277
|
+
def reindex
|
278
|
+
@indexes = {}
|
279
|
+
for col in @index_columns
|
280
|
+
@indexes[col] = {}
|
281
|
+
|
282
|
+
for record in @records
|
283
|
+
add_to_index( col, record.send(col), record )
|
284
|
+
end
|
285
|
+
end
|
286
|
+
end
|
287
|
+
|
288
|
+
# Each spreadsheet has its own anonymous record class, and each CSV row instantiates
|
289
|
+
# a record of this class. This is where the getters and setters come from.
|
290
|
+
def create_record_class
|
291
|
+
create_data_accessor_module
|
292
|
+
@record_class = Class.new( CsvMadness::Record )
|
293
|
+
@record_class.spreadsheet = self
|
294
|
+
@record_class.send( :include, @module )
|
295
|
+
end
|
296
|
+
|
297
|
+
# fetch the original headers from the CSV file. If opts[:headers] is false,
|
298
|
+
# or the CSV file isn't loaded yet, returns an empty array.
|
299
|
+
def fetch_csv_headers
|
300
|
+
if @csv && @opts[:header]
|
301
|
+
if @csv.headers == true
|
302
|
+
@csv.shift
|
303
|
+
headers = @csv.headers
|
304
|
+
@csv.rewind # shift/rewind, else @csv.headers only returns 'true'
|
305
|
+
else
|
306
|
+
headers = @csv.headers
|
307
|
+
end
|
308
|
+
headers
|
309
|
+
else
|
310
|
+
[]
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
# sets the list of columns.
|
315
|
+
# If passed nil:
|
316
|
+
# if the CSV file has headers, will try to create column names based on headers.
|
317
|
+
# otherwise, you end up with accessors like record.col1, record.col2, record.col3...
|
318
|
+
# If the columns given doesn't match the number of columns in the spreadsheet
|
319
|
+
# prints a warning and a comparison of the columns to the headers.
|
320
|
+
def set_initial_columns( columns = nil )
|
321
|
+
if columns.nil?
|
322
|
+
if @opts[:header] == false #
|
323
|
+
@columns = (0...csv_column_count).map{ |i| :"col#{i}" }
|
324
|
+
else
|
325
|
+
@columns = fetch_csv_headers.map{ |name| self.class.getter_name( name ) }
|
326
|
+
end
|
327
|
+
else
|
328
|
+
@columns = columns
|
329
|
+
unless @columns.length == csv_column_count
|
330
|
+
puts "Warning <#{@spreadsheet_file}>: columns array does not match the number of columns in the spreadsheet."
|
331
|
+
compare_columns_to_headers
|
332
|
+
end
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
# Printout so the user can see which CSV columns are being matched to which
|
337
|
+
# getter/setters. Helpful for debugging dropped or duplicate entries in the
|
338
|
+
# column list.
|
339
|
+
def compare_columns_to_headers
|
340
|
+
headers = fetch_csv_headers
|
341
|
+
|
342
|
+
for i in 0...([@columns, headers].map(&:length).max)
|
343
|
+
puts "\t#{i}: #{@columns[i]} ==> #{headers[i]}"
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
|
348
|
+
# Create objects that respond to the recipe-named methods
|
349
|
+
def package
|
350
|
+
@records = []
|
351
|
+
@csv.each do |row|
|
352
|
+
@records << @record_class.new( row )
|
353
|
+
end
|
354
|
+
end
|
355
|
+
|
356
|
+
# How many columns? Based off the length of the headers.
|
357
|
+
def csv_column_count
|
358
|
+
fetch_csv_headers.length
|
359
|
+
end
|
360
|
+
|
361
|
+
def columns_to_mapping
|
362
|
+
@columns.each_with_index.inject({}){ |memo, item|
|
363
|
+
memo[item.first] = item.last
|
364
|
+
memo
|
365
|
+
}
|
366
|
+
end
|
367
|
+
|
368
|
+
def create_data_accessor_module
|
369
|
+
# columns = @columns # yes, this line is necessary. Module.new has its own @vars.
|
370
|
+
|
371
|
+
@module = DataAccessorModule.new( columns_to_mapping )
|
372
|
+
end
|
373
|
+
|
374
|
+
def update_data_accessor_module
|
375
|
+
@module.remap_accessors( columns_to_mapping )
|
376
|
+
end
|
377
|
+
end
|
378
|
+
end
|
data/test/csv/simple.csv
ADDED
data/test/helper.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'bundler'
|
3
|
+
|
4
|
+
begin
|
5
|
+
Bundler.setup(:default, :development)
|
6
|
+
rescue Bundler::BundlerError => e
|
7
|
+
$stderr.puts e.message
|
8
|
+
$stderr.puts "Run `bundle install` to install missing gems"
|
9
|
+
exit e.status_code
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'test/unit'
|
13
|
+
require 'shoulda'
|
14
|
+
|
15
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
16
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
17
|
+
|
18
|
+
require 'csv_madness'
|
19
|
+
|
20
|
+
class Test::Unit::TestCase
|
21
|
+
end
|
@@ -0,0 +1,232 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestCsvMadness < Test::Unit::TestCase
|
4
|
+
context "all:" do
|
5
|
+
setup do
|
6
|
+
@csv_search_path = Pathname.new( __FILE__ ).dirname.join("csv")
|
7
|
+
@csv_output_path = @csv_search_path.join("out")
|
8
|
+
CsvMadness::Sheet.add_search_path( @csv_search_path )
|
9
|
+
end
|
10
|
+
|
11
|
+
teardown do
|
12
|
+
if defined?(FileUtils)
|
13
|
+
FileUtils.rm_rf( Dir.glob( @csv_output_path.join("**","*") ) )
|
14
|
+
else
|
15
|
+
puts "fileutils not defined"
|
16
|
+
`rm -rf #{@csv_output_path.join('*')}`
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
context "testing sheet basics" do
|
21
|
+
should "not accept duplicate search paths" do
|
22
|
+
CsvMadness::Sheet.add_search_path( Pathname.new( __FILE__ ).dirname.join("csv") )
|
23
|
+
assert_equal 1, CsvMadness::Sheet.instance_variable_get("@search_paths").length
|
24
|
+
end
|
25
|
+
|
26
|
+
should "load a simple spreadsheet" do
|
27
|
+
simple = CsvMadness::Sheet.from("simple.csv")
|
28
|
+
assert_equal "Mary", simple[0].fname
|
29
|
+
assert_equal "Paxton", simple[1].lname
|
30
|
+
assert_equal "72", simple[2].age
|
31
|
+
assert_equal "1", simple[0].id
|
32
|
+
assert_equal [:id, :fname, :lname, :age, :born], simple.columns
|
33
|
+
assert_equal [:id, :fname, :lname, :age, :born], simple[0].columns
|
34
|
+
end
|
35
|
+
|
36
|
+
should "index records properly on a simple spreadsheet, using custom columns" do
|
37
|
+
simple = CsvMadness.load( "simple.csv",
|
38
|
+
columns: [:index, :given_name, :surname, :years_old, :born],
|
39
|
+
index: [:index] )
|
40
|
+
bill = simple.fetch(:index, "2")
|
41
|
+
mary = simple.fetch(:index, "1")
|
42
|
+
|
43
|
+
assert_equal "Bill", bill.given_name
|
44
|
+
assert_equal "Moore", mary.surname
|
45
|
+
assert_equal "1", mary.index
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
context "testing transformations" do
|
50
|
+
context "with a simple spreadsheet loaded" do
|
51
|
+
setup do
|
52
|
+
@simple = CsvMadness.load( "simple.csv", index: [:id] )
|
53
|
+
end
|
54
|
+
|
55
|
+
teardown do
|
56
|
+
@simple = nil
|
57
|
+
end
|
58
|
+
|
59
|
+
should "transform every cell" do
|
60
|
+
@simple.alter_cells do |cell|
|
61
|
+
cell = "*#{cell}*"
|
62
|
+
end
|
63
|
+
|
64
|
+
bill = @simple.fetch(:id, "2")
|
65
|
+
mary = @simple.fetch(:id, "1")
|
66
|
+
|
67
|
+
assert_equal "*Bill*", bill.fname
|
68
|
+
assert_equal "*Moore*", mary.lname
|
69
|
+
assert_equal "*1*", mary.id
|
70
|
+
end
|
71
|
+
|
72
|
+
should "transform every cell, accessing the whole record" do
|
73
|
+
@simple.alter_cells do |cell, record|
|
74
|
+
cell == record.id ? record.id : "#{record.id}: #{cell}"
|
75
|
+
end
|
76
|
+
|
77
|
+
bill = @simple.fetch(:id, "2")
|
78
|
+
mary = @simple.fetch(:id, "1")
|
79
|
+
|
80
|
+
assert_equal "2: Bill", bill.fname
|
81
|
+
assert_equal "1: Moore", mary.lname
|
82
|
+
assert_equal "1", mary.id
|
83
|
+
end
|
84
|
+
|
85
|
+
should "transform id column using alter_column" do
|
86
|
+
@simple.alter_column(:id) do |id|
|
87
|
+
(id.to_i * 2).to_s
|
88
|
+
end
|
89
|
+
|
90
|
+
assert_equal %W(2 4 6 8), @simple.column(:id)
|
91
|
+
end
|
92
|
+
|
93
|
+
should "set ID column to integers" do
|
94
|
+
@simple.set_column_type(:id, :integer)
|
95
|
+
|
96
|
+
@simple.records.each do |record|
|
97
|
+
assert_kind_of Integer, record.id
|
98
|
+
assert_includes [1,2,3,4], record.id
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
should "set ID column to floats" do
|
103
|
+
@simple.set_column_type(:id, :float)
|
104
|
+
|
105
|
+
@simple.records.each do |record|
|
106
|
+
assert_kind_of Float, record.id
|
107
|
+
assert_includes [1.0,2.0,3.0,4.0], record.id
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
should "parse a date column with one invalid date" do
|
112
|
+
@simple.set_column_type( :born, :date )
|
113
|
+
born1 = @simple[0].born
|
114
|
+
born3 = @simple[2].born
|
115
|
+
assert_kind_of Time, born1
|
116
|
+
assert_in_delta Time.parse("1986-04-08"), born1, 3600 * 24
|
117
|
+
|
118
|
+
assert_kind_of String, born3
|
119
|
+
assert_match /Invalid Time Format/, born3
|
120
|
+
end
|
121
|
+
|
122
|
+
should "successfully decorate record objects with new functionality" do
|
123
|
+
moduul = Module.new do
|
124
|
+
def full_name
|
125
|
+
"#{self.fname} #{self.lname}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def name_last_first
|
129
|
+
"#{self.lname}, #{self.fname}"
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
@simple.add_record_methods( moduul )
|
134
|
+
|
135
|
+
assert @simple[0].respond_to?( :full_name )
|
136
|
+
assert_equal "Mary Moore", @simple[0].full_name
|
137
|
+
assert @simple[0].respond_to?( :name_last_first )
|
138
|
+
assert_equal "Moore, Mary", @simple[0].name_last_first
|
139
|
+
end
|
140
|
+
|
141
|
+
should "add methods to record objects (block form)" do
|
142
|
+
@simple.add_record_methods do
|
143
|
+
def full_name
|
144
|
+
"#{self.fname} #{self.lname}"
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
assert @simple[0].respond_to?( :full_name )
|
149
|
+
assert_equal "Mary Moore", @simple[0].full_name
|
150
|
+
end
|
151
|
+
|
152
|
+
should "return an array of objects when feeding fetch() an array" do
|
153
|
+
records = @simple.fetch(:id, ["1","2"])
|
154
|
+
assert_equal records.length, records.compact.length
|
155
|
+
assert_equal 2, records.length
|
156
|
+
assert_equal "1", records.first.id
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
context "loading nil spreadsheet" do
|
162
|
+
setup do
|
163
|
+
@nilsheet = CsvMadness.load( "with_nils.csv", index: [:id] )
|
164
|
+
end
|
165
|
+
|
166
|
+
should "stomp away the nils" do
|
167
|
+
@norris = @nilsheet.fetch(:id, "4")
|
168
|
+
assert_equal nil, @norris.born
|
169
|
+
assert_equal "Chuck", @norris.fname
|
170
|
+
|
171
|
+
@nilsheet.nils_are_blank_strings # should turn every nil into a ''
|
172
|
+
assert_equal "", @norris.born
|
173
|
+
assert_equal "Chuck", @norris.fname
|
174
|
+
end
|
175
|
+
|
176
|
+
should "to_csv properly" do
|
177
|
+
@to_csv = @nilsheet.to_csv( force_quotes: true )
|
178
|
+
assert_match /"Moore"/, @to_csv
|
179
|
+
assert_match /"age","born"/, @to_csv
|
180
|
+
end
|
181
|
+
|
182
|
+
should "write to an output file properly" do
|
183
|
+
# debugger
|
184
|
+
@outfile = @csv_output_path.join("output_nilfile.csv")
|
185
|
+
@nilsheet.write_to_file( @outfile, force_quotes: true )
|
186
|
+
|
187
|
+
assert File.exist?( @outfile )
|
188
|
+
@to_csv = File.read( @outfile )
|
189
|
+
assert_match /"Moore"/, @to_csv
|
190
|
+
assert_match /"age","born"/, @to_csv
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
context "testing add/remove column transformations" do
|
195
|
+
context "with simple spreadsheet loaded" do
|
196
|
+
setup do
|
197
|
+
load_simple
|
198
|
+
end
|
199
|
+
|
200
|
+
should "add column" do
|
201
|
+
@simple.add_column( :compound ) do |h, record|
|
202
|
+
v = "#{record.fname} #{record.lname} #{record.id}"
|
203
|
+
end
|
204
|
+
|
205
|
+
load_mary
|
206
|
+
assert_equal "Mary Moore 1", @mary.compound
|
207
|
+
end
|
208
|
+
|
209
|
+
should "drop column" do
|
210
|
+
load_mary
|
211
|
+
assert @mary.respond_to?(:lname)
|
212
|
+
assert_equal "Moore", @mary.lname
|
213
|
+
|
214
|
+
@simple.drop_column( :lname )
|
215
|
+
|
216
|
+
assert @mary.respond_to?(:age)
|
217
|
+
assert_equal "Mary", @mary.fname
|
218
|
+
assert_equal false, @mary.respond_to?(:lname)
|
219
|
+
assert_equal "27", @mary.age
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def load_simple
|
226
|
+
@simple = CsvMadness.load( "simple.csv", index: [:id] )
|
227
|
+
end
|
228
|
+
|
229
|
+
def load_mary
|
230
|
+
@mary = @simple.fetch( :id, "1" )
|
231
|
+
end
|
232
|
+
end
|
metadata
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: csv_madness
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.2
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Bryce Anderson
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-06-03 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: shoulda
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :development
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rdoc
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ~>
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '3.12'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '3.12'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: bundler
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.3.0
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.3.0
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: jeweler
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ~>
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: 1.8.4
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ~>
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: 1.8.4
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: debugger
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: CSV Madness removes what little pain is left from Ruby's CSV class. Load
|
95
|
+
a CSV file, and get back an array of objects with customizable getter/setter methods.
|
96
|
+
email: keeputahweird@gmail.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files:
|
100
|
+
- LICENSE.txt
|
101
|
+
- README.rdoc
|
102
|
+
files:
|
103
|
+
- ./Gemfile
|
104
|
+
- ./README.rdoc
|
105
|
+
- ./Rakefile
|
106
|
+
- ./VERSION
|
107
|
+
- ./lib/csv_madness.rb
|
108
|
+
- ./lib/csv_madness/record.rb
|
109
|
+
- ./lib/csv_madness/sheet.rb
|
110
|
+
- ./test/csv/simple.csv
|
111
|
+
- ./test/helper.rb
|
112
|
+
- ./test/test_csv_madness.rb
|
113
|
+
- LICENSE.txt
|
114
|
+
- README.rdoc
|
115
|
+
homepage: http://github.com/darthschmoo/csv_madness
|
116
|
+
licenses:
|
117
|
+
- MIT
|
118
|
+
post_install_message:
|
119
|
+
rdoc_options: []
|
120
|
+
require_paths:
|
121
|
+
- lib
|
122
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
123
|
+
none: false
|
124
|
+
requirements:
|
125
|
+
- - ! '>='
|
126
|
+
- !ruby/object:Gem::Version
|
127
|
+
version: '0'
|
128
|
+
segments:
|
129
|
+
- 0
|
130
|
+
hash: 279847321864815987
|
131
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
132
|
+
none: false
|
133
|
+
requirements:
|
134
|
+
- - ! '>='
|
135
|
+
- !ruby/object:Gem::Version
|
136
|
+
version: '0'
|
137
|
+
requirements: []
|
138
|
+
rubyforge_project:
|
139
|
+
rubygems_version: 1.8.25
|
140
|
+
signing_key:
|
141
|
+
specification_version: 3
|
142
|
+
summary: CSV Madness turns your CSV rows into happycrazy objects.
|
143
|
+
test_files: []
|