lib/dbf/table.rb in dbf-1.3.0 vs lib/dbf/table.rb in dbf-1.5.0

- old
+ new

@@ -1,12 +1,15 @@ module DBF # DBF::Table is the primary interface to a single DBF file and provides # methods for enumerating and searching the records. + + # TODO set record_length to length of actual used column lengths class Table + include Enumerable + DBF_HEADER_SIZE = 32 - FPT_HEADER_SIZE = 512 VERSION_DESCRIPTIONS = { "02" => "FoxBase", "03" => "dBase III without memo file", "04" => "dBase IV without memo file", @@ -19,67 +22,36 @@ "8e" => "dBase IV with SQL table", "f5" => "FoxPro with memo file", "fb" => "FoxPro without memo file" } - attr_reader :column_count # The total number of columns - attr_reader :columns # An array of DBF::Column attr_reader :version # Internal dBase version number - attr_reader :last_updated # Last updated datetime - attr_reader :memo_file_format # :fpt or :dpt - attr_reader :memo_block_size # The block size for memo records - attr_reader :options # The options hash used to initialize the table - attr_reader :data # DBF file handle - attr_reader :memo # Memo file handle attr_reader :record_count # Total number of records # Opens a DBF::Table # Example: # table = DBF::Table.new 'data.dbf' # # @param [String] path Path to the dbf file def initialize(path) @data = File.open(path, 'rb') + get_header_info @memo = open_memo(path) - reload! end # Closes the table and memo file def close + @memo && @memo.close @data.close - @memo.close if @memo end - # Reloads the database and memo files - def reload! - @records = nil - get_header_info - get_memo_header_info - get_column_descriptors - end - - # Checks if there is a memo file - # - # @return [Boolean] - def has_memo_file? - @memo ? true : false - end - - # Retrieve a Column by name - # - # @param [String, Symbol] column_name - # @return [DBF::Column] - def column(column_name) - @columns.detect {|f| f.name == column_name.to_s} - end - # Calls block once for each record in the table. The record may be nil # if the record has been marked as deleted. # # @yield [nil, DBF::Record] def each - 0.upto(@record_count - 1) {|index| yield record(index)} + @record_count.times {|i| yield record(i)} end # Retrieve a record by index number. # The record will be nil if it has been deleted, but not yet pruned from # the database. @@ -118,44 +90,30 @@ # t.column :is_active, :boolean # t.column :age, :integer # t.column :notes, :text # end # - # @param [optional String] path # @return [String] - def schema(path = nil) + def schema s = "ActiveRecord::Schema.define do\n" s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n" columns.each do |column| s << " t.column #{column.schema_definition}" end - s << " end\nend" - - if path - File.open(path, 'w') {|f| f.puts(s)} - end - + s << " end\nend" s end - def to_a - records = [] - each {|record| records << record if record} - records - end - # Dumps all records to a CSV file. If no filename is given then CSV is # output to STDOUT. # # @param [optional String] path Defaults to basename of dbf file def to_csv(path = nil) - path = File.basename(@data.path, '.dbf') + '.csv' if path.nil? - FCSV.open(path, 'w', :force_quotes => true) do |csv| + path = default_csv_path unless path + csv_class.open(path, 'w', :force_quotes => true) do |csv| csv << columns.map {|c| c.name} - each do |record| - csv << record.to_a - end + each {|record| csv << record.to_a} end end # Find records using a simple ActiveRecord-like syntax. # @@ -192,124 +150,78 @@ when :first find_first(options) end end - private - - # Find all matching - # - # @param [Hash] options - # @yield [optional DBF::Record] - # @return [Array] - def find_all(options, &block) - results = [] - each do |record| - if record.try(:match?, options) - if block_given? - yield record - else - results << record - end - end + # Retrieves column information from the database + def columns + return @columns if @columns + + @data.seek(DBF_HEADER_SIZE) + @columns = [] + column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE + column_count.times do + name, type, length, decimal = @data.read(32).unpack('a10 x a x4 C2') + @columns << Column.new(name.strip, type, length, decimal) if length > 0 end - results + @columns end - # Find first matching - # - # @param [Hash] options - # @return [DBF::Record, nil] - def find_first(options) - each do |record| - return record if record.try(:match?, options) - end - nil - end + private - # Open memo file - # - # @params [String] path - # @return [File] - def open_memo(path) + def open_memo(path) #nodoc %w(fpt FPT dbt DBT).each do |extname| - filename = replace_extname(path, extname) + filename = path.sub(/#{File.extname(path)[1..-1]}$/, extname) if File.exists?(filename) - @memo_file_format = extname.downcase.to_sym - return File.open(filename, 'rb') + return Memo.new(File.open(filename, 'rb'), version) end end nil end - # Replace the file extension - # - # @param [String] path - # @param [String] extension - # @return [String] - def replace_extname(path, extension) - path.sub(/#{File.extname(path)[1..-1]}$/, extension) + def find_all(options) #nodoc + map do |record| + if record.try(:match?, options) + yield record if block_given? + record + end + end.compact end - # Is record marked for deletion - # - # @return [Boolean] - def deleted_record? + def find_first(options) #nodoc + each do |record| + return record if record.try(:match?, options) + end + nil + end + + def deleted_record? #nodoc @data.read(1).unpack('a') == ['*'] end def current_record - deleted_record? ? nil : DBF::Record.new(self) + deleted_record? ? nil : DBF::Record.new(@data.read(@record_length), columns, version, @memo) end - # Determine database version, record count, header length and record length - def get_header_info + def get_header_info #nodoc @data.rewind @version, @record_count, @header_length, @record_length = @data.read(DBF_HEADER_SIZE).unpack('H2 x3 V v2') - @column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE end - # Retrieves column information from the database - def get_column_descriptors - @columns = [] - @column_count.times do - name, type, length, decimal = @data.read(32).unpack('a10 x a x4 C2') - if length > 0 - @columns << Column.new(name.strip, type, length, decimal) - end - end - # Reset the column count in case any were skipped - @column_count = @columns.size - - @columns + def seek(offset) #nodoc + @data.seek @header_length + offset end - - # Determines the memo block size and next available block - def get_memo_header_info - if has_memo_file? - @memo.rewind - if @memo_file_format == :fpt - @memo_next_available_block, @memo_block_size = @memo.read(FPT_HEADER_SIZE).unpack('N x2 n') - @memo_block_size = 0 if @memo_block_size.nil? - else - @memo_block_size = 512 - @memo_next_available_block = File.size(@memo.path) / @memo_block_size - end - end + + def seek_to_record(index) #nodoc + seek index * @record_length end - # Seek to a byte offset - # - # @params [Fixnum] offset - def seek(offset) - @data.seek(@header_length + offset) + def csv_class #nodoc + CSV.const_defined?(:Reader) ? FCSV : CSV end - - # Seek to a record - # - # @param [Fixnum] index - def seek_to_record(index) - seek(index * @record_length) + + def default_csv_path #nodoc + File.basename(@data.path, '.dbf') + '.csv' end end end \ No newline at end of file