lib/csv_pirate.rb in csv_pirate-3.4.4 vs lib/csv_pirate.rb in csv_pirate-4.0.1

- old
+ new

@@ -5,16 +5,16 @@ #License: MIT License #Labels: Ruby, Rails, Gem, Plugin #Version: 1.0 #Project owners: # peter.boling (The Cap'n) -require 'faster_csv' unless defined?(FasterCSV) class CsvPirate BOOKIE = [:counter, :timestamp, :none] MOP_HEADS = [:clean, :dirty] + BRIGANTINE = [:first, :last] attr_accessor :waggoner # First part of filename attr_accessor :chart # directory, default is (['log','csv']) attr_accessor :aft # extension, default is ('.csv') attr_accessor :gibbet # part of the filename after waggoner and date, before swabbie and aft @@ -50,11 +50,15 @@ attr_accessor :blackjack # Hash: Specify how you want your CSV header # {:join => '-'} joins the method names called to get hte data for that column with '_' underscores. # {:humanize => '-'} first joins as above, then humanizes the string # {:array => ['col1',col2','col3'] Uses the column names provided in the array. If the array provided is too short defaults to :humanize =>'_' - cattr_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 + attr_accessor :brigantine # the complete file path + attr_accessor :pinnacle # the complete file path + class << self + 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 + end # CsvPirate only works for commissions of swag OR grub! # :swag the ARrr collection of swag to work on (optional) # :grub the ARrr class that the spyglasses will be used on (optional) # :spyglasses named scopes in your model that will refine the rows in the CSV according to conditions of the spyglasses, @@ -79,19 +83,19 @@ # {:humanize =>'-'} first joins as above, then humanizes the string # {:array => ['col1',col2','col3'] Uses the column names provided. If the array provided is too short defaults to :humanize =>'_' # See README for examples def initialize(*args) - raise ArgumentError, "must provide required options" if args.blank? + raise ArgumentError, "must provide required options" if args.nil? @swag = args.first[:swag] @grub = args.first[:grub] # if they provide both - raise ArgumentError, "must provide either :swag or :grub, not both" if !self.swag.blank? && !self.grub.blank? + raise ArgumentError, "must provide either :swag or :grub, not both" if !self.swag.nil? && !self.grub.nil? # if they provide neither - raise ArgumentError, "must provide either :swag or :grub" if self.swag.blank? && self.grub.blank? + raise ArgumentError, "must provide either :swag or :grub" if self.swag.nil? && self.grub.nil? @swab = args.first[:swab] || :counter raise ArgumentError, ":swab is #{self.swab.inspect}, but must be one of #{CsvPirate::BOOKIE.inspect}" unless CsvPirate::BOOKIE.include?(self.swab) @mop = args.first[:mop] || :clean @@ -101,51 +105,57 @@ 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?('.')) @waggoner = args.first[:waggoner] || "#{self.grub || self.swag}" 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 - @booty = args.first[:booty] || [] + @booty = args.first[:booty] || [] # would like to use Class#instance_variables for generic classes 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) || self.booty.empty? @chart = args.first[:chart] || ['log','csv'] 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? @aft = args.first[:aft] || '.csv' - @chronometer = args.first[:chronometer] == false ? false : args.first[:chronometer] || Date.today + @chronometer = args.first[:chronometer] == false ? false : (args.first[:chronometer] || Date.today) - @spyglasses = (args.first[:spyglasses] || [:all]) if self.grub + @spyglasses = (args.first[:spyglasses] || (self.respond_to?(:all) ? [:all] : nil)) if self.grub + @shrouds = args.first[:shrouds] || ',' # for tsv, tab-delimited, "\t" 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) @astrolabe = args.first[:astrolabe] || false @bury_treasure = args.first[:astrolabe] || false @buried_treasure = [] - #Default is different than default for has_csv_pirate because this might gem wants to be clean for use outside rails, and humanize may not always be defined for String class - @blackjack = args.first[:blackjack] || {:join => '_'} + #does not rely on rails humanize! + @blackjack = args.first[:blackjack] || {:humanize => '_'} #Make sure the header array is the same length as the booty columns if self.blackjack.keys.first == :array && (self.blackjack.values.first.length != self.booty.length) @blackjack = {:join => '_'} puts "Warning: :blackjack reset to {:join => '_'} because the length of the :booty is different than the length of the array provided to :blackjack" if CsvPirate.parlance(2) end - # Initialize doesn't write anything to a CSV, + @pinnacle = self.block_and_tackle + + # Initialize doesn't write anything to a CSV, # but does create the traverse_board and opens the waggoner for reading / writing self.northwest_passage unless self.astrolabe # This will contain the text of the csv from this particular execution @maroon = "" # Once the traverse_board (dir) exists, then check if the rhumb_lines (file) already exists, and set our rhumb_lines counter @swabbie = self.insult_swabbie - @nocturnal = File.basename(self.poop_deck) + @brigantine = self.poop_deck(args.first[:brigantine]) + + @nocturnal = File.basename(self.brigantine) + # Then open the rhumb_lines - self.rhumb_lines = File.open(File.expand_path(self.poop_deck),self.astrolabe ? "r" : "a") + self.rhumb_lines = File.open(File.expand_path(self.brigantine),self.astrolabe ? "r" : "a") end def self.create(*args) csv_pirate = CsvPirate.new({ :chart => args.first[:chart], @@ -232,11 +242,11 @@ end def jolly_roger if self.bury_treasure if self.buried_treasure.is_a?(Array) - puts "Found #{self.buried_treasure.length} deniers buried here: '#{self.poop_deck}'" if CsvPirate.parlay && CsvPirate.parlance(1) + puts "Found #{self.buried_treasure.length} deniers buried here: '#{self.brigantine}'" if CsvPirate.parlay && CsvPirate.parlance(1) puts "You must weigh_anchor to review your plunder!" if CsvPirate.parlay && CsvPirate.parlance(1) else puts "Failed to locate treasure" if CsvPirate.parlay && CsvPirate.parlance(1) end end @@ -250,39 +260,10 @@ csv << moidore # |x| marks the spot! self.buried_treasure << moidore if self.bury_treasure end end - # create the header of the CSV (column/method names) - # returns an array of strings for CSV header based on blackjack - def block_and_tackle - self.blackjack.map do |k,v| - case k - #Joining is only relevant when the booty contains a nested hash of method calls as at least one of the booty array elements - #Use the booty (methods) as the column headers - when :join then self.binnacle(v, false) - #Use the humanized booty (methods) as the column headers - when :humanize then self.binnacle(v, true) - when :array then v - end - end.first - end - - #returns an array of strings for CSV header based on booty - def binnacle(join_value, humanize = true) - self.booty.map do |compass| - string = compass.is_a?(Hash) ? - self.run_through(compass, join_value) : - compass.is_a?(String) ? - compass : - compass.is_a?(Symbol) ? - compass.to_s : - compass.to_s - humanize ? string.humanize : string - end - end - #Takes a potentially nested hash element of a booty array and turns it into a string for a column header # hash = {:a => {:b => {:c => {:d => {"e" => "fghi"}}}}} # run_through(hash, '_') # => "a_b_c_d_e_fghi" # Ooooh so recursive! @@ -295,34 +276,106 @@ end end.first end #complete file path - def poop_deck - "#{self.analemma}#{self.swabbie}#{self.aft}" + def poop_deck(brig) + if BRIGANTINE.include?(brig) + self.old_csv_dump(brig) + elsif brig.is_a?(String) + "#{self.analemma}#{brig}" + else + "#{self.analemma}#{self.swabbie}#{self.aft}" + end end - # Swabs the poop_deck if the mop is clean. (!) + # Swabs the poop_deck (of the brigantine) if the mop is clean. (!) def swab_poop_deck - self.rhumb_lines.truncate(0) if self.swab == :none && self.mop == :clean && File.size(self.poop_deck) > 0 + self.rhumb_lines.truncate(0) if self.swab == :none && self.mop == :clean && File.size(self.brigantine) > 0 end # Must be done on order to rummage through the loot found by the pirate ship def weigh_anchor - CsvPirate.rinse(self.poop_deck) + CsvPirate.rinse(self.brigantine) end # Sink your own ship! Or run a block of code on each row of the current CSV def scuttle(&block) return false unless block_given? - CsvPirate.broadside(self.poop_deck) do |careen| + CsvPirate.broadside(self.brigantine) do |careen| yield careen end end + def to_memory(exclude_id = true, exclude_timestamps = true) + return nil unless self.grub + begin + example = self.grub.new + rescue Exception + 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 CsvPirate.parlance(1) + example = nil + end + buccaneers = [] + self.scuttle do |row| + #puts "#{self.pinnacle.first.inspect} #{row[self.pinnacle.first].inspect}" + buccaneers << self.grub.new(self.data_hash_from_row(row, exclude_id, exclude_timestamps, example)) + end + buccaneers + end + + def data_hash_from_row(row, exclude_id = true, exclude_timestamps = true, example = nil) + data_hash = {} + my_booty = self.booty.reject {|x| x.is_a?(Hash)} + my_booty = exclude_id ? my_booty.reject {|x| a = x.to_sym; [:id, :ID,:dbid, :DBID, :db_id, :DB_ID].include?(a)} : self.booty + my_booty = exclude_timestamps ? my_booty.reject {|x| a = x.to_sym; [:created_at, :updated_at, :created_on, :updated_on].include?(a)} : self.booty + my_booty = my_booty.reject {|x| !example.respond_to?("#{x}=".to_sym)} unless example.nil? + my_booty.each do |method| + #puts "#{self.pinnacle[index]}" + data_hash = data_hash.merge({method => row[self.pinnacle[self.booty.index(method)]]}) + end + #puts "#{data_hash.inspect}" + data_hash + end + + # Grab an old CSV dump (first or last) + def old_csv_dump(brig) + file = Dir.entries(self.traverse_board).reject {|x| x.match(/^\./)}.sort.send(brig) + "#{self.traverse_board}#{file}" + end + protected + # create the header of the CSV (column/method names) + # returns an array of strings for CSV header based on blackjack + def block_and_tackle + self.blackjack.map do |k,v| + case k + #Joining is only relevant when the booty contains a nested hash of method calls as at least one of the booty array elements + #Use the booty (methods) as the column headers + when :join then self.binnacle(v, false) + #Use the humanized booty (methods) as the column headers + when :humanize then self.binnacle(v, true) + when :array then v + end + end.first + end + + #returns an array of strings for CSV header based on booty + def binnacle(join_value, humanize = true) + self.booty.map do |compass| + string = compass.is_a?(Hash) ? + self.run_through(compass, join_value) : + compass.is_a?(String) ? + compass : + compass.is_a?(Symbol) ? + compass.to_s : + compass.to_s + humanize ? string.to_s.gsub(/_id$/, "").gsub(/_/, " ").capitalize : string + end + end + + # The directory path to the csv def traverse_board #If we have rails environment then we use rails root, otherwise self.chart stands on its own as a relative path "#{self.north_pole}#{self.chart.join('/')}/" end @@ -356,22 +409,25 @@ def filibuster(flotsam) base = File.basename(flotsam, self.aft) index = base.rindex('.') tail = index.nil? ? nil : base[index+1,base.length] # Ensure numbers - counter = tail.nil? ? 0 : tail[/\d*/].to_i + tail.nil? ? 0 : tail[/\d*/].to_i end - + + # Get all the files in the dir + def axe + Dir.glob(self.lantern) + end + + # File increment for next CSV to dump def boatswain return self.swabbie unless self.swabbie.nil? - counter = 0 - bowspirit = Dir.glob(self.lantern) highval = 0 - bowspirit.each do |flotsam| + self.axe.each do |flotsam| counter = self.filibuster(flotsam) highval = ((highval <=> counter) == 1) ? highval : counter - counter = 0 end ".#{highval + 1}" end def coxswain @@ -407,11 +463,11 @@ def self.marlinespike(spoils, navigation) navigation.map do |east,west| spoils = spoils.send(east.to_sym) unless spoils.nil? if west.is_a?(Hash) - # Recursive nadness is here! + # Recursive madness is here! spoils = CsvPirate.marlinespike(spoils, west) else spoils = spoils.send(west.to_sym) end end @@ -429,12 +485,10 @@ end # Sink other ships! Or run a block of code on each row of a CSV def self.broadside(galley, &block) return false unless block_given? - count = 1 if report_kills FasterCSV.foreach(galley, {:headers => :first_row, :return_headers => false}) do |gun| - puts "Galleys sunk: #{count+=1}" if CsvPirate.parlance(1) yield gun end end # During a mutiny things are a little different!