lib/dbf/table.rb in dbf-1.5.2 vs lib/dbf/table.rb in dbf-1.5.3

- old
+ new

@@ -1,16 +1,16 @@ module DBF - # DBF::Table is the primary interface to a single DBF file and provides + # 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 - + VERSION_DESCRIPTIONS = { "02" => "FoxBase", "03" => "dBase III without memo file", "04" => "dBase IV without memo file", "05" => "dBase V without memo file", @@ -21,61 +21,62 @@ "8b" => "dBase IV with memo file", "8e" => "dBase IV with SQL table", "f5" => "FoxPro with memo file", "fb" => "FoxPro without memo file" } - - attr_reader :version # Internal dBase version number - attr_reader :record_count # Total number of records - + + attr_reader :version # Internal dBase version number + attr_reader :record_count # Total number of records + attr_accessor :encoding # Source encoding (for ex. :cp1251) + # 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) end - + # Closes the table and memo file def close @memo && @memo.close @data.close 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 @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. # # @param [Fixnum] index # @return [DBF::Record, NilClass] def record(index) seek_to_record(index) current_record end - + alias_method :row, :record - + # Human readable version description # # @return [String] def version_description VERSION_DESCRIPTIONS[version] end - + # Generate an ActiveRecord::Schema - # + # # xBase data types are converted to generic types as follows: # - 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 @@ -97,44 +98,43 @@ 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" + s << " end\nend" s 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 = default_csv_path unless path - csv_class.open(path, 'w', :force_quotes => true) do |csv| + csv_class.open(path || default_csv_path, 'w', :force_quotes => true) do |csv| csv << columns.map {|c| c.name} each {|record| csv << record.to_a} 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 + # 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 @@ -149,80 +149,83 @@ find_all(options, &block) when :first find_first(options) 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 + @columns ||= begin + column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE + + @data.seek(DBF_HEADER_SIZE) + columns = [] + 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, @encoding) if length > 0 + end + columns end - @columns end - + private - + def open_memo(path) #nodoc %w(fpt FPT dbt DBT).each do |extname| filename = path.sub(/#{File.extname(path)[1..-1]}$/, extname) if File.exists?(filename) return Memo.new(File.open(filename, 'rb'), version) end end nil end - + def find_all(options) #nodoc map do |record| if record.try(:match?, options) yield record if block_given? record end end.compact end - + def find_first(options) #nodoc - each do |record| - return record if record.try(:match?, options) - end - nil + detect {|record| record.try(:match?, options)} end - + def deleted_record? #nodoc @data.read(1).unpack('a') == ['*'] end - - def current_record + + def current_record #nodoc deleted_record? ? nil : DBF::Record.new(@data.read(@record_length), columns, version, @memo) end - + def get_header_info #nodoc @data.rewind - @version, @record_count, @header_length, @record_length = @data.read(DBF_HEADER_SIZE).unpack('H2 x3 V v2') + @version, @record_count, @header_length, @record_length, encoding_key = + @data.read(DBF_HEADER_SIZE).unpack("H2 x3 V v2 x17H2") + @encoding = self.class.encodings[encoding_key] if "".respond_to? :encoding end - + def seek(offset) #nodoc @data.seek @header_length + offset end - + def seek_to_record(index) #nodoc seek index * @record_length end - + def csv_class #nodoc CSV.const_defined?(:Reader) ? FCSV : CSV end - + def default_csv_path #nodoc File.basename(@data.path, '.dbf') + '.csv' end - + + def self.encodings + @encodings ||= YAML.load_file(File.expand_path("../encodings.yml", __FILE__)) + end end - -end \ No newline at end of file + +end