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