motion-csv 0.0.1
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 +15 -0
- data/README.md +41 -0
- data/lib/motion-csv.rb +8 -0
- data/lib/motion-csv/motion-csv.rb +427 -0
- data/lib/motion-csv/version.rb +3 -0
- metadata +62 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
YWEyYmZjNDU5YzI4ZGFmODBjZmQxODc3OWMyMTEyM2VmZmNjMTUyMw==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
ZmY5N2E0MzM0YjZlZDBkMmNiZjZiM2YwZDRjZDljNjg4Nzg3Y2E4OA==
|
7
|
+
!binary "U0hBNTEy":
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
Y2E1NmIyM2E4NDFlM2FlMmMwYmQ3ZDEwODEyN2RmMDhiMGQyYzJhMTYyZDA1
|
10
|
+
M2U3MTMwNTUxM2I4ZDViNzFjZWY0MDYyNDU1ZGQ2YmY4MGRiN2EyYzBjMDk0
|
11
|
+
ZWJlMTVjYWJiMjViNzE5NGJjZmQwMGZlNjhjNmMyN2NmN2RjYzY=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
YjczNjRhM2NjMTJlMTQ1YWFkMzAwZmRlNmJhZjc1ZjU3NjlhYzBjYjBhNTE1
|
14
|
+
NmMzMDA4MGNiZWIzZTUzMjExNDc0MTY0MDdlOTk5ZmExNTE4Njk2MWI4ZGZk
|
15
|
+
OTQyYTU3Yzk5MTFmM2E0MDhiOGVkNTQ1MDM5MThlMWZjODRiZjc=
|
data/README.md
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
# motion-csv
|
2
|
+
|
3
|
+
This is a RubyMotion friendly port of fasterer-csv by Mason: http://rubygems.org/gems/fasterer-csv
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'motion-csv'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install motion-csv
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
Check out the `specs` dorectory for usage examples, but here's a brief example:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
csv_string = "a,b,c,d
|
25
|
+
1,2,3,4
|
26
|
+
5,6,7,whatever"
|
27
|
+
|
28
|
+
csv = MotionCSV.parse(csv_string)
|
29
|
+
|
30
|
+
puts csv.headers # [:a, :b, :c, :d]
|
31
|
+
puts csv.first[:b] # 2
|
32
|
+
puts csv.last[:d] # "whatever"
|
33
|
+
```
|
34
|
+
|
35
|
+
## Contributing
|
36
|
+
|
37
|
+
1. Fork it
|
38
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
39
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
40
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
41
|
+
5. Create new Pull Request
|
data/lib/motion-csv.rb
ADDED
@@ -0,0 +1,8 @@
|
|
1
|
+
unless defined?(Motion::Project::Config)
|
2
|
+
raise "This file must be required within a RubyMotion project Rakefile."
|
3
|
+
end
|
4
|
+
|
5
|
+
lib_dir_path = File.dirname(File.expand_path(__FILE__))
|
6
|
+
Motion::Project::App.setup do |app|
|
7
|
+
app.files.unshift(Dir.glob(File.join(lib_dir_path, "motion-csv/**/*.rb")))
|
8
|
+
end
|
@@ -0,0 +1,427 @@
|
|
1
|
+
module MotionCSV
|
2
|
+
|
3
|
+
class Table < Array
|
4
|
+
|
5
|
+
class << self
|
6
|
+
def format_headers(unformatted)
|
7
|
+
unformatted.map { |header| Row.to_key(header) }
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
attr_reader :headers, :lines, :line_block
|
12
|
+
|
13
|
+
def initialize(headers, fail_on_malformed_columns = true, &line_block)
|
14
|
+
@headers = Table.format_headers(headers)
|
15
|
+
@fail_on_malformed_columns = fail_on_malformed_columns
|
16
|
+
@line_block = line_block
|
17
|
+
@lines = 0
|
18
|
+
@indexes = {}
|
19
|
+
end
|
20
|
+
|
21
|
+
def <<(row)
|
22
|
+
@lines += 1
|
23
|
+
if !row.is_a?(Row)
|
24
|
+
row = Row.new(self, row, @lines)
|
25
|
+
end
|
26
|
+
if @headers.length != row.length
|
27
|
+
error = "*** WARNING - COLUMN COUNT MISMATCH - WARNING ***\n*** ROW #{size} : EXPECTED #{@headers.length} : FOUND #{row.length}\n\n"
|
28
|
+
len = 0
|
29
|
+
headers.each do |header|
|
30
|
+
len = header.to_s.length if header.to_s.length > len
|
31
|
+
end
|
32
|
+
headers.each_with_index do |header, i|
|
33
|
+
error << sprintf("%-32s : %s\n", header, row[i])
|
34
|
+
end
|
35
|
+
puts error
|
36
|
+
raise error if @fail_on_malformed_columns
|
37
|
+
end
|
38
|
+
if line_block
|
39
|
+
line_block.call(row)
|
40
|
+
else
|
41
|
+
super(row)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
alias_method :push, :<<
|
45
|
+
|
46
|
+
def merge(*tables)
|
47
|
+
|
48
|
+
tables.each do |table|
|
49
|
+
matching = self.headers & table.headers
|
50
|
+
|
51
|
+
key = {}
|
52
|
+
|
53
|
+
table.each do |row|
|
54
|
+
matching.each do |match|
|
55
|
+
key[match] = row[match]
|
56
|
+
end
|
57
|
+
|
58
|
+
self.lookup(key) { |r| r.merge(row) }
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
self
|
63
|
+
|
64
|
+
end
|
65
|
+
|
66
|
+
def index(columns, reindex = false)
|
67
|
+
columns = columns.compact.uniq.sort { |a, b| a.to_s <=> b.to_s }.map { |column| Row.to_key(column) }
|
68
|
+
|
69
|
+
key = columns.join('|#|')
|
70
|
+
|
71
|
+
@indexes[key] ||= {}
|
72
|
+
|
73
|
+
index = @indexes[key]
|
74
|
+
|
75
|
+
if reindex || index.empty?
|
76
|
+
|
77
|
+
self.each do |row|
|
78
|
+
vkey = columns.map { |column| row[column] }
|
79
|
+
index[vkey] ||= []
|
80
|
+
index[vkey] << row
|
81
|
+
end
|
82
|
+
end
|
83
|
+
index
|
84
|
+
end
|
85
|
+
|
86
|
+
def lookup(key)
|
87
|
+
|
88
|
+
values = []
|
89
|
+
columns = key.keys.compact.uniq.sort { |a, b| a.to_s <=> b.to_s }.map do |column|
|
90
|
+
values << key[column]
|
91
|
+
Row.to_key(column)
|
92
|
+
end
|
93
|
+
|
94
|
+
rows = index(columns)[values]
|
95
|
+
if rows && block_given?
|
96
|
+
rows.each do |row|
|
97
|
+
yield(row)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
rows
|
102
|
+
end
|
103
|
+
|
104
|
+
def write(file, quot = '"', sep = ',')
|
105
|
+
MotionCSV.write(file, quot, sep) do |out|
|
106
|
+
out << headers
|
107
|
+
each do |row|
|
108
|
+
out << row
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
alias_method :rows, :to_a
|
114
|
+
alias_method :merge!, :merge
|
115
|
+
|
116
|
+
end
|
117
|
+
|
118
|
+
class Row < Array
|
119
|
+
|
120
|
+
class << self
|
121
|
+
def to_key(key)
|
122
|
+
key = "#{key}".downcase.gsub(/\s+/, '_')
|
123
|
+
key.empty? ? :_ : key.to_sym
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
def headers
|
128
|
+
@headers ||= @table.headers.dup
|
129
|
+
end
|
130
|
+
|
131
|
+
attr_reader :line
|
132
|
+
|
133
|
+
def initialize(table, array, line=-1)
|
134
|
+
@table = table
|
135
|
+
@line = line
|
136
|
+
super(array)
|
137
|
+
end
|
138
|
+
|
139
|
+
def [](*is)
|
140
|
+
is.each do |i|
|
141
|
+
val = if i.is_a? Fixnum
|
142
|
+
super
|
143
|
+
else
|
144
|
+
found = headers.index(Row::to_key(i))
|
145
|
+
found ? super(found) : nil
|
146
|
+
end
|
147
|
+
return val unless val.nil?
|
148
|
+
end
|
149
|
+
nil
|
150
|
+
end
|
151
|
+
|
152
|
+
def []=(key, val)
|
153
|
+
if key.is_a? Fixnum
|
154
|
+
super
|
155
|
+
else
|
156
|
+
key = Row::to_key(key)
|
157
|
+
headers << key unless headers.include? key
|
158
|
+
found = headers.index(key)
|
159
|
+
super(found, val)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
|
163
|
+
def pull(*columns)
|
164
|
+
columns.map do |column|
|
165
|
+
column = [nil] if column.nil?
|
166
|
+
self[*column]
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def merge(row)
|
171
|
+
if row.is_a? Row
|
172
|
+
row.headers.each do |header|
|
173
|
+
self[header] = row[header]
|
174
|
+
end
|
175
|
+
else
|
176
|
+
row.each do |key, value|
|
177
|
+
self[key] = value
|
178
|
+
end
|
179
|
+
end
|
180
|
+
self
|
181
|
+
end
|
182
|
+
|
183
|
+
def to_hash
|
184
|
+
headers.inject({}) do |memo, h|
|
185
|
+
memo[h] = self[h]
|
186
|
+
memo
|
187
|
+
end
|
188
|
+
end
|
189
|
+
|
190
|
+
def key?(key)
|
191
|
+
keys.include?(Row.to_key(key))
|
192
|
+
end
|
193
|
+
|
194
|
+
def value?(value)
|
195
|
+
values.include?(value)
|
196
|
+
end
|
197
|
+
|
198
|
+
def method_missing(method, *args, &block)
|
199
|
+
to_hash.send(method, *args, &block)
|
200
|
+
end
|
201
|
+
|
202
|
+
alias_method :keys, :headers
|
203
|
+
alias_method :values, :to_a
|
204
|
+
|
205
|
+
alias_method :has_key?, :key?
|
206
|
+
alias_method :member?, :key?
|
207
|
+
alias_method :include?, :key?
|
208
|
+
|
209
|
+
alias_method :has_value?, :value?
|
210
|
+
alias_method :merge!, :merge
|
211
|
+
|
212
|
+
end
|
213
|
+
|
214
|
+
class NumericConversion < Array
|
215
|
+
|
216
|
+
def initialize
|
217
|
+
@int = @float = true
|
218
|
+
@dot = false
|
219
|
+
end
|
220
|
+
|
221
|
+
def clear
|
222
|
+
@int = @float = true
|
223
|
+
@dot = false
|
224
|
+
super
|
225
|
+
end
|
226
|
+
|
227
|
+
def <<(ch)
|
228
|
+
if ch == ?-.ord
|
229
|
+
@float = @int = size == 0
|
230
|
+
elsif (ch > ?9.ord || ch < ?0.ord) && ch != ?..ord
|
231
|
+
@int = @float = false
|
232
|
+
elsif ch == ?..ord && @dot
|
233
|
+
@int = @float = false
|
234
|
+
elsif ch == ?..ord
|
235
|
+
@int = false
|
236
|
+
@dot = true
|
237
|
+
end
|
238
|
+
|
239
|
+
super(ch.chr)
|
240
|
+
end
|
241
|
+
|
242
|
+
def convert(as_string = false)
|
243
|
+
if as_string
|
244
|
+
join
|
245
|
+
elsif empty?
|
246
|
+
nil
|
247
|
+
elsif @int
|
248
|
+
join.to_i
|
249
|
+
elsif @float
|
250
|
+
join.to_f
|
251
|
+
else
|
252
|
+
join
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
end
|
257
|
+
|
258
|
+
class NoConversion < Array
|
259
|
+
|
260
|
+
def <<(ch)
|
261
|
+
super(ch.chr)
|
262
|
+
end
|
263
|
+
|
264
|
+
def convert(as_string = false)
|
265
|
+
if as_string
|
266
|
+
join
|
267
|
+
elsif empty?
|
268
|
+
nil
|
269
|
+
else
|
270
|
+
join
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
274
|
+
end
|
275
|
+
|
276
|
+
class IOWriter
|
277
|
+
def initialize(file, quot = '"', sep = ',', quotenum = false)
|
278
|
+
@first = true; @io = file; @quot = quot; @sep = sep; @quotenum = quotenum
|
279
|
+
end
|
280
|
+
|
281
|
+
def <<(row)
|
282
|
+
raise "can only write arrays! #{row.class} #{row.inspect}" unless row.is_a? Array
|
283
|
+
if @first && row.is_a?(Row)
|
284
|
+
self.<<(row.headers)
|
285
|
+
end
|
286
|
+
@first = false
|
287
|
+
@io.syswrite MotionCSV::quot_row(row, @quot, @sep, @quotenum)
|
288
|
+
row
|
289
|
+
end
|
290
|
+
end
|
291
|
+
|
292
|
+
class << self
|
293
|
+
|
294
|
+
def headers(file, quot = '"', sep = ',', fail_on_malformed = true, column = NoConversion.new, &block)
|
295
|
+
parse_headers(File.open(file, 'r') { |io| io.gets }, quot, sep, fail_on_malformed, column, &block)
|
296
|
+
end
|
297
|
+
|
298
|
+
def read(file, quot = '"', sep = ',', fail_on_malformed = true, column = NoConversion.new, &block)
|
299
|
+
File.open(file, 'r') do |io|
|
300
|
+
parse(io, quot, sep, fail_on_malformed, column, &block)
|
301
|
+
end
|
302
|
+
end
|
303
|
+
|
304
|
+
def convread(file, quot = '"', sep = ',', fail_on_malformed = true, column = NumericConversion.new, &block)
|
305
|
+
File.open(file, 'r') do |io|
|
306
|
+
parse(io, quot, sep, fail_on_malformed, column, &block)
|
307
|
+
end
|
308
|
+
end
|
309
|
+
|
310
|
+
def parse_headers(data, quot = '"', sep = ',', fail_on_malformed = true, column = NoConversion.new, &block)
|
311
|
+
parse(data, quot, sep, fail_on_malformed, column, &block).headers
|
312
|
+
end
|
313
|
+
|
314
|
+
def parse(io, quot = '"', sep = ',', fail_on_malformed = true, column = NoConversion.new, &block)
|
315
|
+
q, s, row, inquot, clean, maybe, table, field, endline = quot.ord, sep.ord, [], false, true, false, nil, true, false
|
316
|
+
|
317
|
+
io.each_byte do |c|
|
318
|
+
next if c == ?\r.ord
|
319
|
+
|
320
|
+
if maybe && c == s
|
321
|
+
row << column.convert(true)
|
322
|
+
column.clear
|
323
|
+
clean, inquot, maybe, field, endline = true, false, false, true, false
|
324
|
+
elsif maybe && c == ?\n.ord && table.nil?
|
325
|
+
row << column.convert(true) unless (column.empty? && endline)
|
326
|
+
column.clear
|
327
|
+
table = Table.new(row, fail_on_malformed, &block) unless row.empty?
|
328
|
+
row, clean, inquot, maybe, field, endline = [], true, false, false, false, true
|
329
|
+
elsif maybe && c == ?\n.ord
|
330
|
+
row << column.convert(true) unless (column.empty? && endline)
|
331
|
+
column.clear
|
332
|
+
table << row unless row.empty?
|
333
|
+
row, clean, inquot, maybe, field, endline = [], true, false, false, false, true
|
334
|
+
elsif clean && c == q
|
335
|
+
inquot, clean, endline = true, false, false
|
336
|
+
elsif maybe && c == q
|
337
|
+
column << c
|
338
|
+
clean, maybe, endline = false, false, false
|
339
|
+
elsif c == q
|
340
|
+
maybe, endline = true, false
|
341
|
+
elsif inquot
|
342
|
+
column << c
|
343
|
+
clean, endline = false, false
|
344
|
+
elsif c == s
|
345
|
+
row << column.convert(false)
|
346
|
+
column.clear
|
347
|
+
clean, field, endline = true, true, false
|
348
|
+
elsif c == ?\n.ord && table.nil?
|
349
|
+
|
350
|
+
row << column.convert(false) unless column.empty? && endline
|
351
|
+
|
352
|
+
column.clear
|
353
|
+
table = Table.new(row, fail_on_malformed, &block) unless row.empty?
|
354
|
+
row, clean, inquot, field, endline = [], true, false, false, true
|
355
|
+
elsif c == ?\n.ord
|
356
|
+
|
357
|
+
row << column.convert(false) unless column.empty? && endline
|
358
|
+
|
359
|
+
column.clear
|
360
|
+
table << row unless row.empty?
|
361
|
+
row, clean, inquot, field, endline = [], true, false, false, true
|
362
|
+
else
|
363
|
+
column << c
|
364
|
+
clean, endline = false, false
|
365
|
+
end
|
366
|
+
end
|
367
|
+
|
368
|
+
if !clean
|
369
|
+
row << column.convert(maybe)
|
370
|
+
if table
|
371
|
+
table << row unless row.empty?
|
372
|
+
else
|
373
|
+
table = Table.new(row, fail_on_malformed, &block) unless row.empty?
|
374
|
+
end
|
375
|
+
elsif field
|
376
|
+
row << column.convert(maybe)
|
377
|
+
end
|
378
|
+
|
379
|
+
table
|
380
|
+
end
|
381
|
+
|
382
|
+
def quot_row(row, q = '"', s = ',', numquot = false)
|
383
|
+
num_quot = /(?:[#{q}#{s}\n]|^\d+$)/
|
384
|
+
need_quot = /[#{q}#{s}\n]/
|
385
|
+
row.map do |val|
|
386
|
+
if val.nil?
|
387
|
+
""
|
388
|
+
elsif val.is_a? Numeric
|
389
|
+
val.to_s
|
390
|
+
else
|
391
|
+
quot = (val.is_a?(Symbol) || !numquot) ? need_quot : num_quot
|
392
|
+
val = String(val)
|
393
|
+
if val.length == 0
|
394
|
+
q * 2
|
395
|
+
else
|
396
|
+
val[quot] ? q + val.gsub(q, q * 2) + q : val
|
397
|
+
end
|
398
|
+
end
|
399
|
+
end.join(s) + "\n"
|
400
|
+
end
|
401
|
+
|
402
|
+
def generate(quot = '"', sep = ',', &block)
|
403
|
+
builder = StringIO.new
|
404
|
+
write(builder, quot, sep, &block)
|
405
|
+
builder.string
|
406
|
+
end
|
407
|
+
|
408
|
+
def write(data, quot = '"', sep = ',', quotenum = false, &block)
|
409
|
+
out(data, 'w', quot, sep, quotenum, &block)
|
410
|
+
end
|
411
|
+
|
412
|
+
def append(data, quot = '"', sep = ',', quotenum = false, &block)
|
413
|
+
out(data, 'a', quot, sep, quotenum, &block)
|
414
|
+
end
|
415
|
+
|
416
|
+
def out(data, mode = 'w', quot = '"', sep = ',', quotenum = false, &block)
|
417
|
+
if data.class == String
|
418
|
+
File.open(data, mode) do |io|
|
419
|
+
out(io, mode, quot, sep, quotenum, &block)
|
420
|
+
end
|
421
|
+
else
|
422
|
+
yield(IOWriter.new(data, quot, sep, quotenum))
|
423
|
+
end
|
424
|
+
end
|
425
|
+
|
426
|
+
end
|
427
|
+
end
|
metadata
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: motion-csv
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Mark Rickert
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2013-10-14 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rake
|
15
|
+
prerelease: false
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ! '>='
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '0'
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ! '>='
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
type: :development
|
27
|
+
description: ! 'This is a RubyMotion friendly port of fasterer-csv by Mason: http://rubygems.org/gems/fasterer-csv'
|
28
|
+
email:
|
29
|
+
- mjar81@gmail.com
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- README.md
|
35
|
+
- lib/motion-csv/motion-csv.rb
|
36
|
+
- lib/motion-csv/version.rb
|
37
|
+
- lib/motion-csv.rb
|
38
|
+
homepage:
|
39
|
+
licenses:
|
40
|
+
- MIT
|
41
|
+
metadata: {}
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options: []
|
44
|
+
require_paths:
|
45
|
+
- lib
|
46
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
47
|
+
requirements:
|
48
|
+
- - ! '>='
|
49
|
+
- !ruby/object:Gem::Version
|
50
|
+
version: '0'
|
51
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
requirements: []
|
57
|
+
rubyforge_project:
|
58
|
+
rubygems_version: 2.0.7
|
59
|
+
signing_key:
|
60
|
+
specification_version: 4
|
61
|
+
summary: ! 'This is a RubyMotion friendly port of fasterer-csv by Mason: http://rubygems.org/gems/fasterer-csv'
|
62
|
+
test_files: []
|