lib/dbf/reader.rb in dbf-0.4.7 vs lib/dbf/reader.rb in dbf-0.5.0
- old
+ new
@@ -1,120 +1,248 @@
module DBF
class Reader
+ # The total number of fields (columns)
attr_reader :field_count
+ # An array of DBF::Field records
attr_reader :fields
+ # The total number of records. This number includes any deleted records.
attr_reader :record_count
+ # Internal dBase version number
attr_reader :version
+ # Last updated datetime
attr_reader :last_updated
+ # Either :fpt or :dpt
attr_reader :memo_file_format
+ # The block size for memo records
attr_reader :memo_block_size
- def initialize(file)
- @data_file = File.open(file, 'rb')
- @memo_file = open_memo(file)
+ # Initialize a new DBF::Reader.
+ # Example:
+ # reader = DBF::Reader.new 'data.dbf'
+ def initialize(filename)
+ @in_memory = true
+ @data_file = File.open(filename, 'rb')
+ @memo_file = open_memo(filename)
reload!
end
+ # Reloads the database and memo files
def reload!
+ @records = nil
get_header_info
get_memo_header_info if @memo_file
get_field_descriptors
end
+ # Returns true if there is a corresponding memo file
def has_memo_file?
@memo_file ? true : false
end
- def open_memo(file)
- %w(fpt FPT dbt DBT).each do |extension|
- filename = file.sub(/#{File.extname(file)[1..-1]}$/, extension)
- if File.exists?(filename)
- @memo_file_format = extension.downcase.to_sym
- return File.open(filename, 'rb')
- end
- end
- nil
+ # If true, DBF::Reader will load all records into memory. If false, records are retrieved using file I/O.
+ def in_memory?
+ @in_memory
end
+ # Tells DBF::Reader whether to load all records into memory. Defaults to true.
+ # You may need to set this to false if the database is very large in order to reduce memory usage.
+ def in_memory=(boolean)
+ @in_memory = boolean
+ end
+
+ # Returns an instance of DBF::Field for <b>field_name</b>. <b>field_name</b>
+ # can be a symbol or a string.
def field(field_name)
@fields.detect {|f| f.name == field_name.to_s}
end
# An array of all the records contained in the database file
def records
- seek_to_record(0)
- @records ||= Array.new(@record_count) do |i|
- if active_record?
- DBF::Record.new(self, @data_file, @memo_file)
- else
- seek_to_record(i + 1)
- nil
- end
+ if in_memory?
+ @records ||= get_all_records_from_file
+ else
+ get_all_records_from_file
end
end
alias_method :rows, :records
- # Returns the record at <a>index</i> 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.
+ # Returns the record at <tt>index</tt>.
def record(index)
- seek_to_record(index)
- active_record? ? Record.new(self, @data_file, @memo_file) : nil
+ if in_memory?
+ records[index]
+ else
+ get_record_from_file(index)
+ end
end
+ # Find records. Examples:
+ # reader = DBF::Reader.new 'mydata.dbf'
+ #
+ # # Find record number 5
+ # reader.find(5)
+ #
+ # # Find all records for Keith Morrison
+ # reader.find :all, :first_name => "Keith", :last_name => "Morrison"
+ #
+ # # Find first record
+ # reader.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 field 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 = {})
+ case command
+ when Fixnum
+ if !options.empty?
+ raise ArgumentError, "options are not allowed when command is a record number"
+ end
+ record(command)
+ when :all
+ records.select do |record|
+ options.map {|key, value| record[key.to_s] == value}.all?
+ end
+ when :first
+ return records.first if options.empty?
+ records.detect do |record|
+ options.map {|key, value| record[key.to_s] == value}.all?
+ end
+ end
+ end
+
alias_method :row, :record
+ # Returns a description of the current database file.
def version_description
VERSION_DESCRIPTIONS[version]
end
+ # Returns a database schema in the portable ActiveRecord::Schema format.
+ #
+ # xBase data types are converted to generic types as follows:
+ # - Number fields are converted to :integer if there are no decimals, otherwise
+ # they are converted to :float
+ # - Date fields are converted to :datetime
+ # - Logical fields are converted to :boolean
+ # - Memo fields are converted to :text
+ # - Character fields are converted to :string and the :limit option is set
+ # to the length of the character field
+ #
+ # Example:
+ # create_table "mydata" do |t|
+ # t.column :name, :string, :limit => 30
+ # t.column :last_update, :datetime
+ # t.column :is_active, :boolean
+ # t.column :age, :integer
+ # t.column :notes, :text
+ # end
+ def schema(path = nil)
+ s = "ActiveRecord::Schema.define do\n"
+ s << " create_table \"#{File.basename(@data_file.path, ".*")}\" do |t|\n"
+ fields.each do |field|
+ s << " t.column \"#{field.name}\""
+ case field.type
+ when "N" # number
+ if field.decimal > 0
+ s << ", :float"
+ else
+ s << ", :integer"
+ end
+ when "D" # date
+ s << ", :datetime"
+ when "L" # boolean
+ s << ", :boolean"
+ when "M" # memo
+ s << ", :text"
+ else
+ s << ", :string, :limit => #{field.length}"
+ end
+ s << "\n"
+ end
+ s << " end\nend"
+
+ if path
+ return File.open(path, 'w') {|f| f.puts(s)}
+ else
+ return s
+ end
+ end
+
private
- # Returns false if the record has been marked as deleted, otherwise it returns true. When dBase records are deleted a
- # flag is set, marking the record as deleted. The record will not be fully removed until the database has been compacted.
- def active_record?
- @data_file.read(1).unpack('H2').to_s == '20'
- rescue
- false
- end
+ def open_memo(file)
+ %w(fpt FPT dbt DBT).each do |extension|
+ filename = file.sub(/#{File.extname(file)[1..-1]}$/, extension)
+ if File.exists?(filename)
+ @memo_file_format = extension.downcase.to_sym
+ return File.open(filename, 'rb')
+ end
+ end
+ nil
+ end
- def get_header_info
- @data_file.rewind
- @version, @record_count, @header_length, @record_length = @data_file.read(DBF_HEADER_SIZE).unpack('H2xxxVvv')
- @field_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
- end
+ # Returns false if the record has been marked as deleted, otherwise it returns true. When dBase records are deleted a
+ # flag is set, marking the record as deleted. The record will not be fully removed until the database has been compacted.
+ def active_record?
+ @data_file.read(1).unpack('H2').to_s == '20'
+ rescue
+ false
+ end
- def get_field_descriptors
- @fields = []
- @field_count.times do
- name, type, length, decimal = @data_file.read(32).unpack('a10xax4CC')
- if length > 0 && !name.strip.empty?
- @fields << Field.new(name, type, length, decimal)
+ def get_header_info
+ @data_file.rewind
+ @version, @record_count, @header_length, @record_length = @data_file.read(DBF_HEADER_SIZE).unpack('H2xxxVvv')
+ @field_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
+ end
+
+ def get_field_descriptors
+ @fields = []
+ @field_count.times do
+ name, type, length, decimal = @data_file.read(32).unpack('a10xax4CC')
+ if length > 0 && !name.strip.empty?
+ @fields << Field.new(name, type, length, decimal)
+ end
end
+ # adjust field count
+ @field_count = @fields.size
+ @fields
end
- # adjust field count
- @field_count = @fields.size
- @fields
- end
- def get_memo_header_info
- @memo_file.rewind
- if @memo_file_format == :fpt
- @memo_next_available_block, @memo_block_size = @memo_file.read(FPT_HEADER_SIZE).unpack('Nxxn')
- else
- @memo_block_size = 512
- @memo_next_available_block = File.size(@memo_file.path) / @memo_block_size
+ def get_memo_header_info
+ @memo_file.rewind
+ if @memo_file_format == :fpt
+ @memo_next_available_block, @memo_block_size = @memo_file.read(FPT_HEADER_SIZE).unpack('Nxxn')
+ else
+ @memo_block_size = 512
+ @memo_next_available_block = File.size(@memo_file.path) / @memo_block_size
+ end
end
- end
- def seek(offset)
- @data_file.seek(@header_length + offset)
- end
+ def seek(offset)
+ @data_file.seek(@header_length + offset)
+ end
- def seek_to_record(index)
- seek(index * @record_length)
- end
+ def seek_to_record(index)
+ seek(index * @record_length)
+ 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(index)
+ active_record? ? Record.new(self, @data_file, @memo_file) : nil
+ end
+
+ def get_all_records_from_file
+ seek_to_record(0)
+ Array.new(@record_count) do |i|
+ active_record? ? DBF::Record.new(self, @data_file, @memo_file) : nil
+ end
+ end
end
end
\ No newline at end of file