module DBF DBF_HEADER_SIZE = 32 FPT_HEADER_SIZE = 512 FPT_BLOCK_HEADER_SIZE = 8 class Reader attr_reader :field_count attr_reader :fields attr_reader :record_count 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 get_header_info get_memo_header_info if @memo_file get_field_descriptors end def get_header_info @data_file.rewind @record_count, @header_length, @record_length = @data_file.read(DBF_HEADER_SIZE).unpack('xxxxVvv') @field_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE end def get_memo_header_info @memo_file.rewind @memo_next_available_block, @memo_block_size = @memo_file.read(FPT_HEADER_SIZE).unpack('Nxxn') end def has_memo_file? @memo_file ? true : false end def get_field_descriptors @fields = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('a10xax4ss'))} end def field(field_name) @fields.select {|f| f.name == field_name} 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) end memo_string end def unpack_string(field) @data_file.read(field.length).unpack("a#{field.length}") end def records seek(0) Array.new(@record_count) {build_record if @data_file.read(1).unpack('c').to_s == '32'} end alias_method :rows, :records def record(index) seek_to_record(index) build_record if @data_file.read(1).unpack('c').to_s == '32' end def build_record record = Record.new @fields.each do |field| case field.type when 'N' # number record[field.name] = unpack_string(field)[0].to_i when 'D' # date raw = unpack_string(field).to_s.strip record[field.name] = raw.strip.empty? ? nil : Date.new(*raw.match(/([\d]{4})([\d]{2})([\d]{2})/).to_a.slice(1,3).map {|n| n.to_i}) rescue nil when 'M' # memo starting_block = unpack_string(field).first.to_i record[field.name] = starting_block == 0 ? nil : memo(starting_block) when 'L' # logical record[field.name] = unpack_string(field) =~ /(y|t)/i ? true : false else record[field.name] = unpack_string(field).to_s.strip end end record end def seek(offset) @data_file.seek(@header_length + offset) end def seek_to_record(index) seek(@record_length * index) end end class Field attr_accessor :name, :type, :length, :decimal def initialize(name, type, length, decimal) self.name, self.type, self.length, self.decimal = name, type, length, decimal end def name=(name) @name = name.gsub(/\0/, '') end end class Record < Hash end end