lib/dbf/reader.rb in dbf-0.3.0 vs lib/dbf/reader.rb in dbf-0.4.0
- old
+ new
@@ -1,32 +1,39 @@
-require 'rubygems'
-require 'breakpoint'
module DBF
DBF_HEADER_SIZE = 32
FPT_HEADER_SIZE = 512
FPT_BLOCK_HEADER_SIZE = 8
DATE_REGEXP = /([\d]{4})([\d]{2})([\d]{2})/
- VERSION_DESCRIPTIONS = {"02" => "FoxBase", "03" => "dBase III without memo file", "04" => "dBase IV without memo file",
- "05" => "dBase V without memo file", "30" => "Visual FoxPro", "31" => "Visual FoxPro with AutoIncrement field",
- "7b" => "dBase IV with memo file", "83" => "dBase III with memo file", "8b" => "dBase IV with memo file",
- "8e" => "dBase IV with SQL table", "f5" => "FoxPro with memo file", "fb" => "FoxPro without memo file"}
+ VERSION_DESCRIPTIONS = {
+ "02" => "FoxBase",
+ "03" => "dBase III without memo file",
+ "04" => "dBase IV without memo file",
+ "05" => "dBase V without memo file",
+ "30" => "Visual FoxPro",
+ "31" => "Visual FoxPro with AutoIncrement field",
+ "7b" => "dBase IV with memo file",
+ "83" => "dBase III with memo file",
+ "8b" => "dBase IV with memo file",
+ "8e" => "dBase IV with SQL table",
+ "f5" => "FoxPro with memo file",
+ "fb" => "FoxPro without memo file"
+ }
class DBFError < StandardError; end
class UnpackError < DBFError; end
class Reader
-
attr_reader :field_count
attr_reader :fields
attr_reader :record_count
attr_reader :version
attr_reader :last_updated
def initialize(file)
@data_file = File.open(file, 'rb')
- @memo_file = File.open(file.gsub(/dbf$/i, 'fpt'), 'rb') rescue File.open(file.gsub(/dbf$/i, 'FPT'), 'rb') rescue nil
+ @memo_file = open_memo(file)
reload!
end
def reload!
get_header_info
@@ -36,34 +43,64 @@
def has_memo_file?
@memo_file ? true : false
end
+ def open_memo(file)
+ %w(fpt FPT dbt DBT).each do |extension|
+ filename = file.sub(/dbf$/i, extension)
+ if File.exists?(filename)
+ @memo_file_format = extension.downcase.to_sym
+ return File.open(filename)
+ end
+ end
+ nil
+ end
+
def field(field_name)
@fields.detect {|f| f.name == field_name.to_s}
end
def memo(start_block)
@memo_file.rewind
@memo_file.seek(start_block * @memo_block_size)
- memo_type, memo_size, memo_string = @memo_file.read(@memo_block_size).unpack("NNa56")
- if memo_size > @memo_block_size - FPT_BLOCK_HEADER_SIZE
- memo_string << @memo_file.read(memo_size - @memo_block_size + FPT_BLOCK_HEADER_SIZE)
+ if @memo_file_format == :fpt
+ memo_type, memo_size, memo_string = @memo_file.read(@memo_block_size).unpack("NNa56")
+ if memo_size > @memo_block_size - FPT_BLOCK_HEADER_SIZE
+ memo_string << @memo_file.read(memo_size - @memo_block_size + FPT_BLOCK_HEADER_SIZE)
+ end
+ else
+ if version == "83" # dbase iii
+ memo_string = ""
+ loop do
+ memo_string << block = @memo_file.read(512)
+ break if block.strip.size < 512
+ end
+ end
end
memo_string
end
+ # An array of all the records contained in the database file
def records
seek_to_record(0)
- @records ||= Array.new(@record_count) { build_record if active_record? }
+ @records ||= Array.new(@record_count) do |i|
+ if active_record?
+ build_record
+ else
+ seek_to_record(i + 1)
+ nil
+ end
+ end
end
alias_method :rows, :records
+ # Jump to record
def record(index)
seek_to_record(index)
- build_record if active_record?
+ active_record? ? build_record : nil
end
alias_method :row, :record
def version_description
@@ -79,18 +116,20 @@
def build_record
record = Record.new
@fields.each do |field|
case field.type
when 'N' # number
- if field.decimal == 0
- record[field.name] = unpack_integer(field) rescue nil
- else
- record[field.name] = unpack_float(field) rescue nil
- end
+ record[field.name] = field.decimal == 0 ? unpack_integer(field) : unpack_float(field) rescue nil
when 'D' # date
raw = unpack_string(field).to_s.strip
- record[field.name] = raw.empty? ? nil : Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}) rescue nil
+ unless raw.empty?
+ begin
+ record[field.name] = Time.gm(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
+ rescue
+ record[field.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}) rescue nil
+ end
+ end
when 'M' # memo
starting_block = unpack_integer(field)
record[field.name] = starting_block == 0 ? nil : memo(starting_block) rescue nil
when 'L' # logical
record[field.name] = unpack_string(field) =~ /^(y|t)$/i ? true : false rescue false
@@ -106,15 +145,20 @@
@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 = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('a10xax4cc'))}
+ @fields = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('a10xax4CC'))}
end
def get_memo_header_info
@memo_file.rewind
- @memo_next_available_block, @memo_block_size = @memo_file.read(FPT_HEADER_SIZE).unpack('Nxxn')
+ 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
def seek(offset)
@data_file.seek(@header_length + offset)
end