lib/dbf/table.rb in dbf-3.1.0 vs lib/dbf/table.rb in dbf-3.1.1

- old
+ new

@@ -35,11 +35,10 @@ '31' => 'Visual FoxPro with AutoIncrement field', 'f5' => 'FoxPro with memo file', 'fb' => 'FoxPro without memo file' } - attr_reader :header attr_accessor :encoding attr_writer :name # Opens a DBF::Table # Examples: @@ -62,24 +61,15 @@ # @param [String, StringIO] data Path to the dbf file or a StringIO object # @param [optional String, StringIO] memo Path to the memo file or a StringIO object # @param [optional String, Encoding] encoding Name of the encoding or an Encoding object def initialize(data, memo = nil, encoding = nil) @data = open_data(data) - @data.rewind - @header = Header.new(@data.read DBF_HEADER_SIZE) @encoding = encoding || header.encoding @memo = open_memo(data, memo) yield self if block_given? - rescue Errno::ENOENT - raise DBF::FileNotFoundError, "file not found: #{data}" end - # @return [TrueClass, FalseClass] - def has_memo_file? - !!@memo - end - # Closes the table and memo file # # @return [TrueClass, FalseClass] def close @data.close @@ -93,74 +83,37 @@ else @data.closed? end end - # @return String - def filename - File.basename @data.path if @data.respond_to?(:path) + # Column names + # + # @return [String] + def column_names + columns.map(&:name) end - # @return String - def name - @name ||= filename && File.basename(filename, ".*") + # All columns + # + # @return [Array] + def columns + @columns ||= build_columns 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 header.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. - # - # @param [Fixnum] index - # @return [DBF::Record, NilClass] - def record(index) - seek_to_record(index) - return nil if deleted_record? - DBF::Record.new(@data.read(header.record_length), columns, version, @memo) - end - - alias_method :row, :record - - # Internal dBase version number - # # @return [String] - def version - @version ||= header.version + def filename + File.basename(@data.path) if @data.respond_to?(:path) end - # Total number of records - # - # @return [Fixnum] - def record_count - @record_count ||= header.record_count - end - - # Human readable version description - # - # @return [String] - def version_description - VERSIONS[version] - 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 STDOUT - def to_csv(path = nil) - out_io = path ? File.open(path, 'w') : $stdout - csv = CSV.new(out_io, force_quotes: true) - csv << column_names - each { |record| csv << record.to_a } - end - # Find records using a simple ActiveRecord-like syntax. # # Examples: # table = DBF::Table.new 'mydata.dbf' # @@ -179,40 +132,82 @@ # matched exactly with the value in the database. If you specify more # than one key, all values must match in order for the record to be # returned. The equivalent SQL would be "WHERE key1 = 'value1' # AND key2 = 'value2'". # - # @param [Fixnum, Symbol] command + # @param [Integer, Symbol] command # @param [optional, Hash] options Hash of search parameters # @yield [optional, DBF::Record, NilClass] def find(command, options = {}, &block) case command - when Fixnum + when Integer record(command) when Array command.map { |i| record(i) } when :all find_all(options, &block) when :first find_first(options) end end - # All columns + # @return [TrueClass, FalseClass] + def has_memo_file? + !!@memo + end + + # @return [String] + def name + @name ||= filename && File.basename(filename, ".*") + end + + # Retrieve a record by index number. + # The record will be nil if it has been deleted, but not yet pruned from + # the database. # - # @return [Array] - def columns - @columns ||= build_columns + # @param [Integer] index + # @return [DBF::Record, NilClass] + def record(index) + seek_to_record(index) + return nil if deleted_record? + DBF::Record.new(@data.read(header.record_length), columns, version, @memo) end - # Column names + alias_method :row, :record + + # Total number of records # + # @return [Integer] + def record_count + @record_count ||= header.record_count + 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 STDOUT + def to_csv(path = nil) + out_io = path ? File.open(path, 'w') : $stdout + csv = CSV.new(out_io, force_quotes: true) + csv << column_names + each { |record| csv << record.to_a } + end + + # Internal dBase version number + # # @return [String] - def column_names - columns.map(&:name) + def version + @version ||= header.version end + # Human readable version description + # + # @return [String] + def version_description + VERSIONS[version] + end + private def build_columns # nodoc @data.seek(DBF_HEADER_SIZE) columns = [] @@ -222,33 +217,63 @@ columns << Column.new(self, name, type, length, decimal) end columns end + def deleted_record? # nodoc + flag = @data.read(1) + flag ? flag.unpack('a') == ['*'] : true + end + def end_of_record? # nodoc original_pos = @data.pos byte = @data.read(1) @data.seek(original_pos) byte.ord == 13 end + def find_all(options) # nodoc + map do |record| + if record && record.match?(options) + yield record if block_given? + record + end + end.compact + end + + def find_first(options) # nodoc + detect { |record| record && record.match?(options) } + end + def foxpro? # nodoc FOXPRO_VERSIONS.keys.include? version end + def header + @header ||= Header.new(@data.read DBF_HEADER_SIZE) + end + def memo_class # nodoc @memo_class ||= begin if foxpro? Memo::Foxpro else version == '83' ? Memo::Dbase3 : Memo::Dbase4 end end end + def memo_search_path(io) # nodoc + dirname = File.dirname(io) + basename = File.basename(io, '.*') + "#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}" + end + def open_data(data) # nodoc data.is_a?(StringIO) ? data : File.open(data, 'rb') + rescue Errno::ENOENT + raise DBF::FileNotFoundError, "file not found: #{data}" end def open_memo(data, memo = nil) # nodoc if memo meth = memo.is_a?(StringIO) ? :new : :open @@ -257,35 +282,11 @@ files = Dir.glob(memo_search_path data) files.any? ? memo_class.open(files.first, version) : nil end end - def memo_search_path(io) # nodoc - dirname = File.dirname(io) - basename = File.basename(io, '.*') - "#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}" - end - - def find_all(options) # nodoc - map do |record| - if record && record.match?(options) - yield record if block_given? - record - end - end.compact - end - - def find_first(options) # nodoc - detect { |record| record && record.match?(options) } - end - - def deleted_record? # nodoc - flag = @data.read(1) - flag ? flag.unpack('a') == ['*'] : true - end - def seek(offset) # nodoc - @data.seek header.header_length + offset + @data.seek(header.header_length + offset) end def seek_to_record(index) # nodoc seek(index * header.record_length) end