lib/dbf/table.rb in dbf-2.0.7 vs lib/dbf/table.rb in dbf-2.0.8
- old
+ new
@@ -1,11 +1,14 @@
module DBF
+ class FileNotFoundError < StandardError
+ end
# DBF::Table is the primary interface to a single DBF file and provides
# methods for enumerating and searching the records.
class Table
include Enumerable
+ include Schema
DBF_HEADER_SIZE = 32
VERSIONS = {
"02" => "FoxBase",
@@ -57,15 +60,19 @@
#
# @param [String, StringIO] data Path to the dbf file or a StringIO object
# @param [optional String, StringIO] memo Path to the memo file or a StringIO object
# @param [optional String, Encoding] encoding Name of the encoding or an Encoding object
def initialize(data, memo = nil, encoding = nil)
- @data = open_data(data)
- @data.rewind
- @header = Header.new(@data.read(DBF_HEADER_SIZE), supports_encoding? || supports_iconv?)
- @encoding = encoding || header.encoding
- @memo = open_memo(data, memo)
+ begin
+ @data = open_data(data)
+ @data.rewind
+ @header = Header.new(@data.read(DBF_HEADER_SIZE), supports_encoding?)
+ @encoding = encoding || header.encoding
+ @memo = open_memo(data, memo)
+ rescue Errno::ENOENT => error
+ raise DBF::FileNotFoundError.new("file not found: #{data}")
+ end
end
# @return [TrueClass, FalseClass]
def has_memo_file?
!!@memo
@@ -106,11 +113,11 @@
# the database.
#
# @param [Fixnum] index
# @return [DBF::Record, NilClass]
def record(index)
- seek(index * header.record_length)
+ seek_to_record(index)
if !deleted_record?
DBF::Record.new(@data.read(header.record_length), columns, version, @memo)
end
end
@@ -135,41 +142,10 @@
# @return [String]
def version_description
VERSIONS[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
- # - 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
- #
- # 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
- #
- # @return [String]
- def schema
- 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
-
# Dumps all records to a CSV file. If no filename is given then CSV is
# output to STDOUT.
#
# @param [optional String] path Defaults to STDOUT
def to_csv(path = nil)
@@ -213,43 +189,63 @@
when :first
find_first(options)
end
end
- # Retrieves column information from the database
+ # All columns
+ #
+ # @return [Array]
def columns
- @columns ||= begin
- @data.seek(DBF_HEADER_SIZE)
- columns = []
- while !["\0", "\r"].include?(first_byte = @data.read(1))
- column_data = first_byte + @data.read(31)
- name, type, length, decimal = column_data.unpack('a10 x a x4 C2')
- if length > 0
- columns << column_class.new(name.strip, type, length, decimal, version, encoding)
- end
- end
- columns
- end
+ @columns ||= build_columns
end
+ # Column names
+ #
+ # @return [String]
+ def column_names
+ columns.map { |column| column.name }
+ end
+
+ # Is string encoding supported?
+ # String encoding is always supported in Ruby 1.9+.
+ # Ruby 1.8.x requires that Ruby be compiled with iconv support.
def supports_encoding?
- String.new.respond_to?(:encoding)
+ supports_string_encoding? || supports_iconv?
end
- def supports_iconv?
+ # Does String support encoding? Should be true in Ruby 1.9+
+ def supports_string_encoding?
+ ''.respond_to?(:encoding)
+ end
+
+ def supports_iconv? #nodoc
require 'iconv'
true
rescue
false
end
- def foxpro?
- FOXPRO_VERSIONS.keys.include? version
+ private
+
+ def build_columns #nodoc
+ columns = []
+ @data.seek(DBF_HEADER_SIZE)
+ while !["\0", "\r"].include?(first_byte = @data.read(1))
+ column_data = first_byte + @data.read(DBF_HEADER_SIZE - 1)
+ name, type, length, decimal = column_data.unpack('a10 x a x4 C2')
+ if length > 0
+ columns << column_class.new(self, name, type, length, decimal)
+ end
+ end
+ columns
end
- private
+ def foxpro? #nodoc
+ FOXPRO_VERSIONS.keys.include? version
+ end
+
def column_class #nodoc
@column_class ||= foxpro? ? Column::Foxpro : Column::Dbase
end
def memo_class #nodoc
@@ -308,9 +304,13 @@
@data.read(1).unpack('a') == ['*']
end
def seek(offset) #nodoc
@data.seek header.header_length + offset
+ end
+
+ def seek_to_record(index) #nodoc
+ seek(index * header.record_length)
end
def csv_class #nodoc
@csv_class ||= CSV.const_defined?(:Reader) ? FCSV : CSV
end