lib/dbf/table.rb in dbf-1.0.11 vs lib/dbf/table.rb in dbf-1.1.0
- old
+ new
@@ -1,122 +1,95 @@
module DBF
+ # DBF::Table is the primary interface to a single DBF file and provides
+ # methods for enumerating and searching the records.
class Table
- include Enumerable
-
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 that was used to initialize the table
+ 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
- # Initializes a new DBF::Table
+ # Opens a DBF::Table
# Example:
# table = DBF::Table.new 'data.dbf'
- def initialize(filename, options = {})
- @data = File.open(filename, 'rb')
- @memo = open_memo(filename)
- @options = options
+ #
+ # @param [String] path Path to the dbf file
+ def initialize(path)
+ @data = File.open(path, 'rb')
+ @memo = open_memo(path)
reload!
end
+ # Closes the table and memo file
+ def 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 if @memo
get_column_descriptors
- build_db_index
end
- # Returns true if there is a corresponding memo file
+ # Checks if there is a memo file
+ #
+ # @return [Boolean]
def has_memo_file?
@memo ? true : false
end
- # The total number of active records.
- def record_count
- @db_index.size
- end
-
- # Returns an instance of DBF::Column for <b>column_name</b>. The <b>column_name</b>
- # can be a specified as either a symbol or string.
+ # 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
- # An array of all the records contained in the database file. Each record is an instance
- # of DBF::Record (or nil if the record is marked for deletion).
- def records
- self.to_a
- end
-
- alias_method :rows, :records
-
+ # 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) do |n|
seek_to_record(n)
- unless deleted_record?
- yield DBF::Record.new(self)
- end
+ yield deleted_record? ? nil : DBF::Record.new(self)
end
end
- # Returns a DBF::Record (or nil if the record has been marked for deletion) for the record at <tt>index</tt>.
+ # Retrieve a record by index number
+ #
+ # @param [Fixnum] index
+ # @return [DBF::Record]
def record(index)
- records[index]
+ seek_to_record(index)
+ deleted_record? ? nil : DBF::Record.new(self)
end
- # Find records using a simple ActiveRecord-like syntax.
- #
- # Examples:
- # table = DBF::Table.new 'mydata.dbf'
- #
- # # Find record number 5
- # table.find(5)
- #
- # # Find all records for Keith Morrison
- # table.find :all, :first_name => "Keith", :last_name => "Morrison"
- #
- # # Find first record
- # table.find :first, :first_name => "Keith"
- #
- # The <b>command</b> can be an id, :all, or :first.
- # <b>options</b> is optional and, if specified, should be a hash where the keys correspond
- # to column names in the database. The values will be 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'".
- def find(command, options = {})
- results = options.empty? ? records : records.select {|record| all_values_match?(record, options)}
-
- case command
- when Fixnum
- record(command)
- when :all
- results
- when :first
- results.first
- end
- end
-
alias_method :row, :record
- # Returns a description of the current database file.
+ # Human readable version description
+ #
+ # @return [String]
def version_description
VERSION_DESCRIPTIONS[version]
end
- # Returns a database schema in the portable ActiveRecord::Schema format.
+ # Generate an ActiveRecord::Schema
#
# xBase data types are converted to generic types as follows:
- # - Number columns are converted to :integer if there are no decimals, otherwise
- # they are converted to :float
+ # - Number columns with no decimals are converted to :integer
+ # - Number columns with decimals are converted to :float
# - Date columns are converted to :datetime
# - Logical columns are converted to :boolean
# - Memo columns are converted to :text
# - Character columns are converted to :string and the :limit option is set
# to the length of the character column
@@ -127,117 +100,196 @@
# t.column :last_update, :datetime
# t.column :is_active, :boolean
# t.column :age, :integer
# t.column :notes, :text
# end
+ #
+ # @param [optional String] path
+ # @return [String]
def schema(path = nil)
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)}
- else
- s
end
+
+ s
end
- # Returns the record at <tt>index</tt> by seeking to the record in the
- # physical database file. See the documentation for the records method for
- # information on how these two methods differ.
- def get_record_from_file(index)
- seek_to_record(@db_index[index])
- Record.new(self)
- end
-
- # Dumps all records into a CSV file
- def to_csv(filename = nil)
- filename = File.basename(@data.path, '.dbf') + '.csv' if filename.nil?
- FCSV.open(filename, 'w', :force_quotes => true) do |csv|
- records.each do |record|
+ # 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|
+ each do |record|
csv << record.to_a
end
end
end
+ # Find records using a simple ActiveRecord-like syntax.
+ #
+ # Examples:
+ # table = DBF::Table.new 'mydata.dbf'
+ #
+ # # Find record number 5
+ # table.find(5)
+ #
+ # # Find all records for Keith Morrison
+ # table.find :all, :first_name => "Keith", :last_name => "Morrison"
+ #
+ # # Find first record
+ # table.find :first, :first_name => "Keith"
+ #
+ # The <b>command</b> may be a record index, :all, or :first.
+ # <b>options</b> is optional and, if specified, should be a hash where the keys correspond
+ # to column names in the database. The values will be 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 [optional, Hash] options Hash of search parameters
+ # @yield [optional, DBF::Record]
+ def find(command, options = {}, &block)
+ case command
+ when Fixnum
+ record(command)
+ when Array
+ command.map {|i| record(i)}
+ when :all
+ find_all(options, &block)
+ when :first
+ find_first(options)
+ end
+ end
+
private
- def open_memo(file)
- %w(fpt FPT dbt DBT).each do |extname|
- filename = replace_extname(file, extname)
- if File.exists?(filename)
- @memo_file_format = extname.downcase.to_sym
- return File.open(filename, 'rb')
+ # Find all matching
+ #
+ # @param [Hash] options
+ # @yield [optional DBF::Record]
+ # @return [Array]
+ def find_all(options, &block)
+ results = []
+ each do |record|
+ if all_values_match?(record, options)
+ if block_given?
+ yield(record)
+ else
+ results << record
end
end
- nil
end
-
- def replace_extname(filename, extension)
- filename.sub(/#{File.extname(filename)[1..-1]}$/, extension)
- end
+ results
+ end
- def deleted_record?
- if @data.read(1).unpack('a') == ['*']
- @data.rewind
- true
- else
- false
- end
+ # Find first matching
+ #
+ # @param [Hash] options
+ # @return [DBF::Record, nil]
+ def find_first(options)
+ each do |record|
+ return record if all_values_match?(record, options)
end
+ nil
+ end
- def get_header_info
- @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
+ # Do all search parameters match?
+ #
+ # @param [DBF::Record] record
+ # @param [Hash] options
+ # @return [Boolean]
+ def all_values_match?(record, options)
+ options.all? {|key, value| record.attributes[key.to_s.underscore] == value}
+ end
- 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
+ # Open memo file
+ #
+ # @params [String] path
+ # @return [File]
+ def open_memo(path)
+ %w(fpt FPT dbt DBT).each do |extname|
+ filename = replace_extname(path, extname)
+ if File.exists?(filename)
+ @memo_file_format = extname.downcase.to_sym
+ return File.open(filename, 'rb')
end
- # Reset the column count in case any were skipped
- @column_count = @columns.size
-
- @columns
end
+ nil
+ end
- def get_memo_header_info
- @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
+ # 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)
+ end
- def seek(offset)
- @data.seek(@header_length + offset)
- end
+ # Is record marked for deletion
+ #
+ # @return [Boolean]
+ def deleted_record?
+ @data.read(1).unpack('a') == ['*']
+ end
- def seek_to_record(index)
- seek(index * @record_length)
- end
-
- def build_db_index
- @db_index = []
- 0.upto(@record_count - 1) do |n|
- seek_to_record(n)
- @db_index << n unless deleted_record?
+ # Determine database version, record count, header length and record length
+ def get_header_info
+ @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
- def all_values_match?(record, options)
- options.map {|key, value| record.attributes[key.to_s.underscore] == value}.all?
+ @columns
+ end
+
+ # Determines the memo block size and next available block
+ def get_memo_header_info
+ @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
+
+ # Seek to a byte offset
+ #
+ # @params [Fixnum] offset
+ def seek(offset)
+ @data.seek(@header_length + offset)
+ end
+
+ # Seek to a record
+ #
+ # @param [Fixnum] index
+ def seek_to_record(index)
+ seek(index * @record_length)
+ end
+
end
end
\ No newline at end of file