csv_pirate 4.1.4 → 5.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,689 @@
1
+ module CsvPirate
2
+ class TheCapn
3
+
4
+ BOOKIE = [:counter, :timestamp, :none]
5
+ MOP_HEADS = [:clean, :dirty]
6
+ BRIGANTINE = [:first, :last]
7
+ CSV_CLASS = (defined?(CSV) ? CSV : FasterCSV)
8
+
9
+ attr_accessor :waggoner # First part of filename
10
+ attr_accessor :chart # directory, default is (['log','csv'])
11
+ attr_accessor :aft # extension, default is ('.csv')
12
+ attr_accessor :gibbet # part of the filename after waggoner and date, before swabbie and aft
13
+ attr_accessor :chronometer # Date object or false
14
+
15
+ # Must provide swag or grub (not both)
16
+ attr_accessor :swag # Array of objects
17
+ attr_accessor :grub # Class
18
+ # spyglasses is only used with grub, not swag
19
+ attr_accessor :spyglasses # named_scopes
20
+
21
+ # These are the booty of the CSV
22
+ # Should be methods/columns on the swag
23
+ # also used to create the figurehead (CSV header)
24
+ attr_accessor :booty # methods / columns
25
+
26
+ attr_accessor :bury_treasure # should we store the csv data in an array for later inspection, or just write the CSV?
27
+ # default is false
28
+ # The array that gets built as we write the CSV... could be useful?
29
+ attr_accessor :buried_treasure # info array
30
+
31
+ attr_accessor :rhumb_lines # the file object to write the CSV lines to
32
+
33
+ attr_accessor :astrolabe # when true then read only CsvPirate instance for loading of CSVs
34
+
35
+ attr_accessor :shrouds # CSV column separator
36
+
37
+ attr_accessor :swab # default is :counter
38
+ attr_accessor :mop # default is :clean (only has an effect if :swab is :none) since overwriting is irrelevant for a new file
39
+ attr_accessor :swabbie # value of the counter / timestamp
40
+ attr_accessor :maroon # text of csv
41
+ attr_accessor :nocturnal # basename of the filepath (i.e. filename)
42
+ attr_accessor :blackjack # Hash: Specify how you want your CSV header
43
+ # {:join => '-'} joins the method names called to get hte data for that column with '_' underscores.
44
+ # {:humanize => '-'} first joins as above, then humanizes the string
45
+ # {:array => ['col1',col2','col3'] Uses the column names provided in the array. If the array provided is too short defaults to :humanize =>'_'
46
+
47
+ attr_accessor :brigantine # the complete file path
48
+ attr_accessor :pinnacle # the complete file path
49
+ class << self
50
+ attr_accessor :parlay # verbosity on a scale of 0 - 3 (0=:none, 1=:error, 2=:info, 3=:debug, 0 being no screen output, 1 is default
51
+ end
52
+
53
+ # CsvPirate only works for commissions of swag OR grub!
54
+ # :swag the ARrr collection of swag to work on (optional)
55
+ # :grub the ARrr class that the spyglasses will be used on (optional)
56
+ # :spyglasses named scopes in your model that will refine the rows in the CSV according to conditions of the spyglasses,
57
+ # and order them according to the order of the spyglasses (optional)
58
+ # :booty booty (columns/methods) on your model that you want printed in the CSV, also used to create the figurehead (CSV header)
59
+ # :chart array of directory names (relative to rails root if using rails) which will be the filepath where you want to hide your loot
60
+ # :wagonner name of document where you will give detailed descriptions of the loot
61
+ # :aft filename extention ('.csv')
62
+ # :shrouds CSV column separator, default is ','. For tsv, tab-delimited, "\t"
63
+ # :chronometer keeps track of when you hunt for treasure, can be false if you don't want to keep track.
64
+ # :gibbet filename spacer after the date, and before the iterative counter/timestamp. MuST contain a '.'
65
+ # :swab can be :counter, :timestamp, or :none
66
+ # :counter - default, each successive run will create a new file using a counter
67
+ # :timestamp - each successive run will create a new file using a HHMMSS time stamp
68
+ # :none - no iterative file naming convention, just use waggoner, aft and gibbet
69
+ # :mop can be :clean or :dirty (:overwrite or :append) (only has an effect if :swab is :none) since overwriting is irrelevant for a new file
70
+ # :clean - do not use :counter or :timestamp, and instead overwrite the file
71
+ # :dirty - do not use :counter, or :timestamp, or :overwrite. Just keep adding on.
72
+ # :bury_treasure should we store the csv data as it is collected in an array in Ruby form for later use (true), or just write the CSV (false)?
73
+ # :blackjack Specify how you want your CSV header
74
+ # {:join => '-'} joins the method names called to get hte data for that column with '_' underscores.
75
+ # {:humanize =>'-'} first joins as above, then humanizes the string
76
+ # {:array => ['col1',col2','col3'] Uses the column names provided. If the array provided is too short defaults to :humanize =>'_'
77
+ # See README for examples
78
+
79
+ def initialize(*args)
80
+ raise ArgumentError, "must provide required options" if args.nil?
81
+
82
+ @swag = args.first[:swag]
83
+ @grub = args.first[:grub]
84
+
85
+ # if they provide both
86
+ raise ArgumentError, "must provide either :swag or :grub, not both" if !self.swag.nil? && !self.grub.nil?
87
+ # if they provide neither
88
+ raise ArgumentError, "must provide either :swag or :grub" if self.swag.nil? && self.grub.nil?
89
+
90
+ @swab = args.first[:swab] || :counter
91
+ raise ArgumentError, ":swab is #{self.swab.inspect}, but must be one of #{TheCapn::BOOKIE.inspect}" unless TheCapn::BOOKIE.include?(self.swab)
92
+
93
+ @mop = args.first[:mop] || :clean
94
+ raise ArgumentError, ":mop is #{self.mop.inspect}, but must be one of #{TheCapn::MOP_HEADS.inspect}" unless TheCapn::MOP_HEADS.include?(self.mop)
95
+
96
+ @gibbet = args.first[:gibbet] || '.export'
97
+ raise ArgumentError, ":gibbet is #{self.gibbet.inspect}, and does not contain a '.' character, which is required when using iterative filenames (set :swab => :none to turn off iterative filenames)" if self.swab != :none && (self.gibbet.nil? || !self.gibbet.include?('.'))
98
+
99
+ @waggoner = args.first[:waggoner] || "#{self.grub || self.swag}"
100
+ raise ArgumentError, ":waggoner is #{self.waggoner.inspect}, and must be a string at least one character long" if self.waggoner.nil? || self.waggoner.length < 1
101
+
102
+ # Not checking if empty here because PirateShip will send [] if the database is unavailable, to allow tasks like rake db:create to run
103
+ @booty = args.first[:booty] || [] # would like to use Class#instance_variables for generic classes
104
+ raise ArgumentError, ":booty is #{self.booty.inspect}, and must be an array of methods to call on a class for CSV data" if self.booty.nil? || !self.booty.is_a?(Array)
105
+
106
+ @chart = args.first[:chart] || ['log','csv']
107
+ raise ArgumentError, ":chart is #{self.chart.inspect}, and must be an array of directory names, which will become the filepath for the csv file" if self.chart.nil? || !self.chart.is_a?(Array) || self.chart.empty?
108
+
109
+ @aft = args.first[:aft] || '.csv'
110
+ @chronometer = args.first[:chronometer] == false ? false : (args.first[:chronometer] || Date.today)
111
+
112
+ @spyglasses = (args.first[:spyglasses] || (self.respond_to?(:all) ? [:all] : nil)) if self.grub
113
+
114
+ @shrouds = args.first[:shrouds] || ',' # for tsv, tab-delimited, "\t"
115
+ raise ArgumentError, ":shrouds is #{self.shrouds.inspect}, and must be a string (e.g. ',' or '\t'), which will be used as the delimeter for the csv file" if self.shrouds.nil? || !self.shrouds.is_a?(String)
116
+
117
+ @astrolabe = args.first[:astrolabe] || false
118
+
119
+ @bury_treasure = args.first[:astrolabe] || false
120
+ @buried_treasure = []
121
+
122
+ #does not rely on rails humanize!
123
+ @blackjack = args.first[:blackjack] || {:humanize => '_'}
124
+
125
+ #Make sure the header array is the same length as the booty columns
126
+ if self.blackjack.keys.first == :array && (self.blackjack.values.first.length != self.booty.length)
127
+ @blackjack = {:join => '_'}
128
+ puts "Warning: :blackjack reset to {:join => '_'} because the length of the :booty is different than the length of the array provided to :blackjack" if TheCapn.parlance(2)
129
+ end
130
+
131
+ @pinnacle = self.block_and_tackle
132
+
133
+ # Initialize doesn't write anything to a CSV,
134
+ # but does create the traverse_board and opens the waggoner for reading / writing
135
+ self.northwest_passage unless self.astrolabe
136
+
137
+ # This will contain the text of the csv from this particular execution
138
+ @maroon = ""
139
+
140
+ # Once the traverse_board (dir) exists, then check if the rhumb_lines (file) already exists, and set our rhumb_lines counter
141
+ @swabbie = self.insult_swabbie
142
+
143
+ @brigantine = self.poop_deck(args.first[:brigantine])
144
+
145
+ @nocturnal = File.basename(self.brigantine)
146
+
147
+
148
+ # Then open the rhumb_lines
149
+ self.rhumb_lines = File.open(File.expand_path(self.brigantine),self.astrolabe ? "r" : "a")
150
+ end
151
+
152
+ def self.create(*args)
153
+ csv_pirate = TheCapn.new({
154
+ :chart => args.first[:chart],
155
+ :aft => args.first[:aft],
156
+ :gibbet => args.first[:gibbet],
157
+ :chronometer => args.first[:chronometer],
158
+ :waggoner => args.first[:waggoner],
159
+ :swag => args.first[:swag],
160
+ :swab => args.first[:swab],
161
+ :shrouds => args.first[:shrouds],
162
+ :mop => args.first[:mop],
163
+ :grub => args.first[:grub],
164
+ :spyglasses => args.first[:spyglasses],
165
+ :booty => args.first[:booty],
166
+ :astrolabe => args.first[:astrolabe],
167
+ :blackjack => args.first[:blackjack],
168
+ :bury_treasure => args.first[:bury_treasure]
169
+ })
170
+ csv_pirate.hoist_mainstay()
171
+ csv_pirate
172
+ end
173
+
174
+ ########################################
175
+ ########### INSTANCE METHODS ###########
176
+ ########################################
177
+
178
+ # This is the hardest working method. Get your shovels!
179
+ def dig_for_treasure(&block)
180
+ return false unless block_given?
181
+
182
+ if !grub.nil?
183
+ self.swag = grub
184
+ spyglasses.each {|x| self.swag = self.swag.send(x) }
185
+ end
186
+
187
+ treasure_chest = self.swag.map do |spoils|
188
+ self.prize(spoils)
189
+ end
190
+
191
+ treasure_chest.each do |loot|
192
+ yield loot
193
+ end
194
+ end
195
+
196
+ def prize(spoils)
197
+ gold_doubloons = []
198
+ self.booty.each do |plunder|
199
+ # Check for nestedness
200
+ if plunder.is_a?(Hash)
201
+ gold_doubloons << TheCapn.marlinespike(spoils, plunder)
202
+ # BJM: if array, assume they are args to be sent along with the function
203
+ elsif plunder.is_a?(Array)
204
+ gold_doubloons << spoils.send(plunder[0].to_sym, *plunder[1..-1] )
205
+ else
206
+ gold_doubloons << spoils.send(plunder.to_sym)
207
+ end
208
+ end
209
+ gold_doubloons
210
+ end
211
+
212
+ def scrivener(msg)
213
+ self.rhumb_lines.puts msg
214
+ end
215
+
216
+ # Sail through your db looking for buried treasure!
217
+ # - restricted to loot that can be seen through spyglasses (if provided)!
218
+ def hoist_mainstay
219
+
220
+ self.swab_poop_deck
221
+
222
+ self.dead_mans_chest
223
+
224
+ self.rhumb_lines.close
225
+
226
+ self.jolly_roger if TheCapn.parlay && TheCapn.parlance(1)
227
+
228
+ # returns the text of this CSV export
229
+ return self.maroon
230
+ end
231
+
232
+ def dead_mans_chest
233
+ self.maroon = CSV_CLASS.generate(:col_sep => self.shrouds) do |csv|
234
+ self.sounding(csv)
235
+ end
236
+ self.scrivener(self.maroon)
237
+ self.maroon
238
+ end
239
+
240
+ def jolly_roger
241
+ if self.bury_treasure
242
+ if self.buried_treasure.is_a?(Array)
243
+ puts "Found #{self.buried_treasure.length} deniers buried here: '#{self.brigantine}'" if TheCapn.parlay && TheCapn.parlance(1)
244
+ puts "You must weigh_anchor to review your plunder!" if TheCapn.parlay && TheCapn.parlance(1)
245
+ else
246
+ puts "Failed to locate treasure" if TheCapn.parlay && TheCapn.parlance(1)
247
+ end
248
+ end
249
+ end
250
+
251
+ def sounding(csv)
252
+ csv << self.block_and_tackle
253
+ # create the data for the csv
254
+ self.dig_for_treasure do |treasure|
255
+ moidore = treasure.map {|x| "#{x}"}
256
+ csv << moidore # |x| marks the spot!
257
+ self.buried_treasure << moidore if self.bury_treasure
258
+ end
259
+ end
260
+
261
+ #Takes a potentially nested hash element of a booty array and turns it into a string for a column header
262
+ # hash = {:a => {:b => {:c => {:d => {"e" => "fghi"}}}}}
263
+ # run_through(hash, '_')
264
+ # => "a_b_c_d_e_fghi"
265
+ # Ooooh so recursive!
266
+ def run_through(hash, join_value)
267
+ hash.map do |k,v|
268
+ if v.is_a?(Hash)
269
+ [k,run_through(v, join_value)].join(join_value)
270
+ else #works for Symbols and Strings
271
+ [k,v].join(join_value)
272
+ end
273
+ end.first
274
+ end
275
+
276
+ #complete file path
277
+ def poop_deck(brig)
278
+ if BRIGANTINE.include?(brig)
279
+ self.old_csv_dump(brig)
280
+ elsif brig.is_a?(String)
281
+ "#{self.analemma}#{brig}"
282
+ else
283
+ "#{self.analemma}#{self.swabbie}#{self.aft}"
284
+ end
285
+ end
286
+
287
+ # Swabs the poop_deck (of the brigantine) if the mop is clean. (!)
288
+ def swab_poop_deck
289
+ self.rhumb_lines.truncate(0) if self.swab == :none && self.mop == :clean && File.size(self.brigantine) > 0
290
+ end
291
+
292
+ # Must be done on order to rummage through the loot found by the pirate ship
293
+ def weigh_anchor
294
+ TheCapn.rinse(self.brigantine)
295
+ end
296
+
297
+ # Sink your own ship! Or run a block of code on each row of the current CSV
298
+ def scuttle(&block)
299
+ return false unless block_given?
300
+ TheCapn.broadside(self.brigantine) do |careen|
301
+ yield careen
302
+ end
303
+ end
304
+
305
+ #permanence can be any of:
306
+ # {:new => :new} - only calls the initializer with data hash for each row to instantiate objects (useful with any vanilla Ruby Class)
307
+ # {:new => :save} - calls the initializer with the data hash for each row and then calls save on each (useful with ActiveRecord)
308
+ # {:new => :create} - calls a create method with the data hash for each row (useful with ActiveRecord)
309
+ # {:find_or_new => [column names for find_by]} - see below (returns only the new objects
310
+ # {:find_or_save => [column names for find_by]} - see below (returns all found or saved objects)
311
+ # {:find_or_create => [column names for find_by]} - looks for existing objects using find_by_#{columns.join('_and_')}, (returns all found or created objects)
312
+ # and if not found creates a new object.
313
+ # The difference between the new, save and create versions are the same as the various :new hashes above.
314
+ # {:update_or_new => [column names for find_by]} - see below (returns only the new objects)
315
+ # {:update_or_save => [column names for find_by]} - see below (returns all updated or saved objects)
316
+ # {:update_or_create => [column names for find_by]} - looks for existing objects using find_by_#{columns.join('_and_')} , (returns all updated or created objects)
317
+ # and updates them with the data hash form the csv row, otherwise creates a new object.
318
+ #TODO: This is a nasty method. Just a quick hack to GTD. Needs to be rethought and refactored. --pboling
319
+ def to_memory(permanence = {:new => :new}, exclude_id = true, exclude_timestamps = true)
320
+ return nil unless self.grub
321
+ begin
322
+ example = self.grub.new
323
+ rescue Exception
324
+ puts "cannot instantiate instance of #{self.grub} with #{self.grub}.new. CsvPirate#to_memory works most reliably when #{self.grub}.new works with no arguments." if TheCapn.parlance(1)
325
+ example = nil
326
+ end
327
+ buccaneers = []
328
+ self.scuttle do |row|
329
+ data_hash = self.data_hash_from_row(row, exclude_id, exclude_timestamps, example)
330
+ case permanence
331
+ when {:new => :new} then
332
+ buccaneers << self.grub.new(data_hash)
333
+ when {:new => :save} then
334
+ obj = self.grub.new(data_hash)
335
+ buccaneers << obj.save(false)
336
+ when {:new => :create} then
337
+ buccaneers << self.grub.create(data_hash)
338
+ else
339
+ if permanence[:find_or_new]
340
+ obj = self.send_aye(data_hash, permanence[:find_or_new])
341
+ buccaneers << self.grub.new(data_hash) if obj.nil?
342
+ elsif permanence[:find_or_save]
343
+ obj = self.send_aye(data_hash, permanence[:find_or_save])
344
+ if obj.nil?
345
+ obj = self.grub.new(data_hash)
346
+ obj.save(false) if obj.respond_to?(:save)
347
+ end
348
+ buccaneers << obj
349
+ elsif permanence[:find_or_create]
350
+ obj = self.send_aye(data_hash, permanence[:find_or_create])
351
+ if obj.nil?
352
+ self.grub.create(data_hash)
353
+ end
354
+ buccaneers << obj
355
+ elsif permanence[:update_or_new]
356
+ obj = self.send_aye(data_hash, permanence[:update_or_new])
357
+ if obj.nil?
358
+ obj = self.grub.new(data_hash)
359
+ else
360
+ self.save_object(obj, data_hash)
361
+ end
362
+ buccaneers << obj
363
+ elsif permanence[:update_or_save]
364
+ obj = self.send_aye(data_hash, permanence[:update_or_save])
365
+ if obj.nil?
366
+ obj = self.grub.new(data_hash)
367
+ obj.save(false)
368
+ else
369
+ self.save_object(obj, data_hash)
370
+ end
371
+ buccaneers << obj
372
+ elsif permanence[:update_or_create]
373
+ obj = self.send_aye(data_hash, permanence[:update_or_create])
374
+ if obj.nil?
375
+ obj = self.grub.create(data_hash)
376
+ else
377
+ self.save_object(obj, data_hash)
378
+ end
379
+ buccaneers << obj
380
+ end
381
+ end
382
+ end
383
+ buccaneers
384
+ end
385
+
386
+ def save_object(obj, data_hash)
387
+ data_hash.each do |k,v|
388
+ obj.send("#{k}=".to_sym, v)
389
+ end
390
+ unless obj.save(false)
391
+ puts "Save Failed: #{obj.inspect}" if TheCapn.parlance(1)
392
+ end
393
+ end
394
+
395
+ def send_aye(data_hash, columns)
396
+ obj = self.grub.send(self.find_aye(columns), self.find_aye_arr(data_hash, columns))
397
+ if obj
398
+ puts "#{self.grub}.#{find_aye(columns)}(#{self.find_aye_arr(data_hash, columns).inspect}): found id = #{obj.id}" if TheCapn.parlance(2)
399
+ end
400
+ end
401
+
402
+ def find_aye(columns)
403
+ "find_by_#{columns.join('_and_')}".to_sym
404
+ end
405
+
406
+ def find_aye_arr(data_hash, columns)
407
+ columns.map do |col|
408
+ data_hash[col.to_s]
409
+ end
410
+ end
411
+
412
+ def data_hash_from_row(row, exclude_id = true, exclude_timestamps = true, example = nil)
413
+ plunder = {}
414
+ my_booty = self.booty.reject {|x| x.is_a?(Hash)}
415
+ my_booty = exclude_id ? my_booty.reject {|x| a = x.to_sym; [:id, :ID,:dbid, :DBID, :db_id, :DB_ID].include?(a)} : self.booty
416
+ my_booty = exclude_timestamps ? my_booty.reject {|x| a = x.to_sym; [:created_at, :updated_at, :created_on, :updated_on].include?(a)} : self.booty
417
+ my_booty = my_booty.reject {|x| !example.respond_to?("#{x}=".to_sym)} unless example.nil?
418
+ my_booty.each do |method|
419
+ plunder = plunder.merge({method => row[self.pinnacle[self.booty.index(method)]]})
420
+ end
421
+ plunder
422
+ end
423
+
424
+ def data_hash_from_row(row, exclude_id = true, exclude_timestamps = true, example = nil)
425
+ data_hash = {}
426
+ my_booty = self.booty.reject {|x| x.is_a?(Hash)}
427
+ my_booty = exclude_id ? my_booty.reject {|x| a = x.to_sym; [:id, :ID,:dbid, :DBID, :db_id, :DB_ID].include?(a)} : self.booty
428
+ my_booty = exclude_timestamps ? my_booty.reject {|x| a = x.to_sym; [:created_at, :updated_at, :created_on, :updated_on].include?(a)} : self.booty
429
+ my_booty = my_booty.reject {|x| !example.respond_to?("#{x}=".to_sym)} unless example.nil?
430
+ my_booty.each do |method|
431
+ data_hash = data_hash.merge({method => row[self.pinnacle[self.booty.index(method)]]})
432
+ end
433
+ data_hash
434
+ end
435
+
436
+ # Grab an old CSV dump (first or last)
437
+ def old_csv_dump(brig)
438
+ file = Dir.entries(self.traverse_board).reject {|x| x.match(/^\./)}.sort.send(brig)
439
+ "#{self.traverse_board}#{file}"
440
+ end
441
+
442
+ protected
443
+
444
+ # create the header of the CSV (column/method names)
445
+ # returns an array of strings for CSV header based on blackjack
446
+ def block_and_tackle
447
+ self.blackjack.map do |k,v|
448
+ case k
449
+ #Joining is only relevant when the booty contains a nested hash of method calls as at least one of the booty array elements
450
+ #Use the booty (methods) as the column headers
451
+ when :join then self.binnacle(v, false)
452
+ #Use the humanized booty (methods) as the column headers
453
+ when :humanize then self.binnacle(v, true)
454
+ when :array then v
455
+ end
456
+ end.first
457
+ end
458
+
459
+ #returns an array of strings for CSV header based on booty
460
+ def binnacle(join_value, humanize = true)
461
+ self.booty.map do |compass|
462
+ string = compass.is_a?(Hash) ?
463
+ self.run_through(compass, join_value) :
464
+ compass.is_a?(String) ?
465
+ compass :
466
+ compass.is_a?(Symbol) ?
467
+ compass.to_s :
468
+ compass.to_s
469
+ humanize ? string.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize : string
470
+ end
471
+ end
472
+
473
+ # The directory path to the csv
474
+ def traverse_board
475
+ #If we have rails environment then we use rails root, otherwise self.chart stands on its own as a relative path
476
+ "#{self.north_pole}#{self.chart.join('/')}/"
477
+ end
478
+
479
+ def sand_glass
480
+ "#{self.chronometer.respond_to?(:strftime) ? '.' + self.chronometer.strftime("%Y%m%d") : ''}"
481
+ end
482
+
483
+ def merchantman
484
+ "#{self.waggoner}#{self.sand_glass}#{self.gibbet}"
485
+ end
486
+
487
+ def analemma
488
+ "#{self.traverse_board}#{self.merchantman}"
489
+ end
490
+
491
+ def north_pole
492
+ "#{defined?(Rails) ? "#{Rails.root}/" : defined?(RAILS_ROOT) ? "#{RAILS_ROOT}/" : ''}"
493
+ end
494
+
495
+ def northwest_passage
496
+ self.chart.length.times do |i|
497
+ north_star = self.north_pole + self.chart[0..i].join('/')
498
+ Dir.mkdir(north_star) if Dir.glob(north_star).empty?
499
+ end
500
+ end
501
+
502
+ def lantern
503
+ "#{self.analemma}.*"
504
+ end
505
+
506
+ def filibuster(flotsam)
507
+ base = File.basename(flotsam, self.aft)
508
+ index = base.rindex('.')
509
+ tail = index.nil? ? nil : base[index+1,base.length]
510
+ # Ensure numbers
511
+ tail.nil? ? 0 : tail[/\d*/].to_i
512
+ end
513
+
514
+ # Get all the files in the dir
515
+ def axe
516
+ Dir.glob(self.lantern)
517
+ end
518
+
519
+ # File increment for next CSV to dump
520
+ def boatswain
521
+ return self.swabbie unless self.swabbie.nil?
522
+ highval = 0
523
+ self.axe.each do |flotsam|
524
+ counter = self.filibuster(flotsam)
525
+ highval = ((highval <=> counter) == 1) ? highval : counter
526
+ end
527
+ ".#{highval + 1}"
528
+ end
529
+
530
+ def coxswain
531
+ return self.swabbie unless self.swabbie.nil?
532
+ ".#{Time.now.strftime("%I%M%S")}"
533
+ end
534
+
535
+ # Sets the swabby file counter
536
+ def insult_swabbie
537
+ return case self.swab
538
+ when :counter
539
+ self.boatswain
540
+ when :timestamp
541
+ self.coxswain
542
+ else
543
+ ""
544
+ end
545
+ end
546
+
547
+ public
548
+
549
+ ########################################
550
+ ############ CLASS METHODS #############
551
+ ########################################
552
+
553
+ # if this is your booty:
554
+ # {:booty => [
555
+ # :id,
556
+ # {:region => {:country => :name }, :state => :name },
557
+ # :name
558
+ # ]}
559
+ # so nested_hash = {:region => {:country => :name }, :state => :name }
560
+ def self.marlinespike(spoils, navigation)
561
+ navigation.map do |east,west|
562
+ # BJM:
563
+ if east.is_a?(Array)
564
+ spoils = spoils.send(east[0].to_sym, *east[1..-1] )
565
+ else
566
+ spoils = spoils.send(east.to_sym)
567
+ end
568
+ unless spoils.nil?
569
+ if west.is_a?(Hash)
570
+ # Recursive madness is here!
571
+ spoils = TheCapn.marlinespike(spoils, west)
572
+ elsif west.is_a?(Array)
573
+ spoils << spoils.send(west[0].to_sym, *west[1..-1] )
574
+ else
575
+ spoils = spoils.send(west.to_sym)
576
+ end
577
+ end
578
+ spoils
579
+ end.compact.join(' - ')
580
+ end
581
+
582
+ # Used to read any loot found by any pirate
583
+ def self.rinse(quarterdeck)
584
+ File.open(File.expand_path(quarterdeck), "r") do |bucket_line|
585
+ bucket_line.each_line do |bucket|
586
+ puts bucket
587
+ end
588
+ end
589
+ end
590
+
591
+ # Sink other ships! Or run a block of code on each row of a CSV
592
+ def self.broadside(galley, &block)
593
+ return false unless block_given?
594
+ CSV_CLASS.foreach(galley, {:headers => :first_row, :return_headers => false}) do |gun|
595
+ yield gun
596
+ end
597
+ end
598
+
599
+ # During a mutiny things are a little different!
600
+ # Essentially you are using an existing CSV to drive queries to create a second CSV cased on the first
601
+ # The capn hash is:
602
+ # :grub => is the class on which to make booty [method] calls
603
+ # :swag => column index in CSV (0th, 1st, 2nd, etc. column?)
604
+ # (swag OR spyglasses can be specified, but code defers to swag if provided)
605
+ # :spyglasses => is the column to load ("find_by_#{booty}") the ARrr object for each row on the first CSV
606
+ # (swag OR spyglasses can be specified, but code defers to swag if provided)
607
+ # :waggoner => where the capn's loot was stashed (filename)
608
+ # :chart => array of directory names where capn's waggoner is located
609
+ # :astrolabe => true (file is opened at top of file in read only mode when true)
610
+ # The first_mate hash is:
611
+ # :grub => is the class on which to make booty [method] calls, or
612
+ # is a method (as a string) we call to get from the object loaded by capn,
613
+ # to the object on which we'll make the first_mate booty [method] calls, or nil, if same object
614
+ # :swag => is the method to call on first CSV row's object to find second CSV row's object (if grub is a class)
615
+ # :spyglasses => is the column to load ("find_by_#{booty}") the ARrr object for each row on the second CSV (if grub is a class)
616
+ # :booty => is the methods to call on the ARrr object for each row on the second CSV
617
+ # :waggoner => where to stash the first mate's loot (filename)
618
+ # :chart => array of directory names where first mate's waggoner is located
619
+ # :astrolabe => false (false is the default for astrolabe, so we could leave it off the first_mate)
620
+ #
621
+ # Example:
622
+ # capn = {:grub => User,:spyglasses => [:inactive],:booty => ['id','login','status'],:waggoner => 'orig',:chart => ['log','csv'],:astrolabe => false}
623
+ # make_orig = CsvPirate.new(capn)
624
+ # make_orig.hoist_mainstay
625
+ # make_orig.weigh_anchor
626
+ #
627
+ # first_mate = {:grub => 'account',:booty => ["id","number","name","created_at"],:waggoner => 'fake',:chart => ['log','csv']}
628
+ # OR
629
+ # # for same class, we re-use the object loaded from first CSV and make the booty [method] calls on it
630
+ # first_mate = {:grub => User,:booty => ["id","login","visits_count"],:waggoner => 'fake',:chart => ['log','csv']}
631
+ # OR
632
+ # first_mate = {:grub => Account,:spyglasses => 'id',:swag=>'user_id',:booty => ["id","name","number"],:waggoner => 'fake',:chart => ['log','csv']}
633
+ # AND
634
+ # capn = {:grub => User,:spyglasses => 'login',:swag => 1,:waggoner => 'orig',:chart => ['log','csv'],:astrolabe => true}
635
+ # after_mutiny = CsvPirate.mutiny(capn, first_mate)
636
+ #
637
+ def self.mutiny(capn, first_mate)
638
+ carrack = TheCapn.new(capn)
639
+ cutthroat = TheCapn.new(first_mate)
640
+
641
+ cutthroat.figurehead
642
+
643
+ carrack.scuttle do |cutlass|
644
+ puts "CUTLASS: #{cutlass.inspect}" if TheCapn.parlance(2)
645
+ puts "CARRACK.SWAG: #{carrack.swag.inspect}" if TheCapn.parlance(2)
646
+ backstaff = cutlass[carrack.swag] || cutlass["#{carrack.spyglasses}"]
647
+ puts "BACKSTAFF: #{backstaff}" if TheCapn.parlance(2)
648
+ puts "CARRACK.SPYGLASSES: #{carrack.spyglasses.inspect}" if TheCapn.parlance(2)
649
+ gully = carrack.grub.send("find_by_#{carrack.spyglasses}".to_sym, backstaff)
650
+ puts "GULLY: #{gully.inspect}" if TheCapn.parlance(2)
651
+ if gully
652
+ flotsam = cutthroat.grub.is_a?(String) ?
653
+ gully.send(cutthroat.grub.to_sym) :
654
+ cutthroat.grub.is_a?(Symbol) ?
655
+ gully.send(cutthroat.grub) :
656
+ cutthroat.grub.class == carrack.grub.class ?
657
+ gully :
658
+ cutthroat.grub.class == Class ?
659
+ cutthroat.grub.send("find_by_#{cutthroat.swag}", gully.send(cutthroat.spyglasses)) :
660
+ nil
661
+ puts "FLOTSAM: #{flotsam.inspect}" if TheCapn.parlance(2)
662
+ if flotsam
663
+ plunder = cutthroat.prize(flotsam)
664
+ cutthroat.buried_treasure << plunder
665
+ cutthroat.scrivener(plunder.map {|bulkhead| "#{bulkhead}"}.join(','))
666
+ else
667
+ puts "Unable to locate: #{cutthroat.grub} related to #{carrack.grub}.#{carrack.spyglasses} '#{gully.send(carrack.spyglasses)}'" if TheCapn.parlance(1)
668
+ end
669
+ else
670
+ puts "Unable to locate: #{carrack.grub}.#{carrack.spyglasses} '#{gully.send(carrack.spyglasses)}'" if TheCapn.parlance(1)
671
+ end
672
+ end
673
+
674
+ carrack.rhumb_lines.close
675
+ cutthroat.rhumb_lines.close
676
+
677
+ cutthroat.jolly_roger
678
+
679
+ # returns the array that is created before exporting it to CSV
680
+ return cutthroat
681
+ end
682
+
683
+ # verbosity on a scale of 0 - 3 (0=:none, 1=:error, 2=:info, 3=:debug, 0 being no screen output, 1 is default
684
+ def self.parlance(level = 1)
685
+ self.parlay.is_a?(Numeric) && self.parlay >= level
686
+ end
687
+
688
+ end
689
+ end