csv_pirate 4.1.4 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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