dbf-5.2.0/0000755000004100000410000000000015166514103012303 5ustar www-datawww-datadbf-5.2.0/bin/0000755000004100000410000000000015166514103013053 5ustar www-datawww-datadbf-5.2.0/bin/dbf0000755000004100000410000000301615166514103013534 0ustar www-datawww-data#!/usr/bin/env ruby Signal.trap('PIPE', 'SYSTEM_DEFAULT') require 'dbf' require 'dbf/version' require 'optparse' params = ARGV.getopts('h', 's', 'a', 'c', 'r', 'v') if params['v'] puts "dbf version: #{DBF::VERSION}" elsif params['h'] puts "usage: #{File.basename(__FILE__)} [-h|-s|-a|-c|-r] filename" puts ' -h = print this message' puts ' -v = print the DBF gem version' puts ' -s = print summary information' puts ' -a = create an ActiveRecord::Schema' puts ' -r = create a Sequel migration' puts ' -c = export as CSV' else filename = ARGV.shift abort 'You must supply a filename on the command line' unless filename # create an ActiveRecord::Schema if params['a'] table = DBF::Table.new filename puts table.schema(:activerecord) end # create an Sequel::Migration if params['r'] table = DBF::Table.new filename puts table.schema(:sequel) end if params['s'] table = DBF::Table.new filename puts puts "Database: #{filename}" puts "Type: (#{table.version}) #{table.version_description}" puts "Encoding: #{table.header_encoding}" if table.header_encoding puts "Memo File: #{table.has_memo_file? ? 'true' : 'false'}" puts "Records: #{table.record_count}" puts "\nFields:" puts 'Name Type Length Decimal' puts '-' * 78 table.columns.each do |f| puts format('%-16s %-10s %-10s %-10s', f.name, f.type, f.length, f.decimal) end end if params['c'] table = DBF::Table.new filename table.to_csv end end dbf-5.2.0/dbf.gemspec0000644000004100000410000000145315166514103014406 0ustar www-datawww-datarequire_relative 'lib/dbf/version' Gem::Specification.new do |s| s.name = 'dbf' s.version = DBF::VERSION s.authors = ['Keith Morrison'] s.email = 'keithm@infused.org' s.homepage = 'https://github.com/infused/dbf' s.summary = 'Read xBase files' s.description = 'A small fast library for reading dBase, xBase, Clipper and FoxPro database files.' s.license = 'MIT' s.bindir = 'bin' s.executables = ['dbf'] s.files = Dir['README.md', 'CHANGELOG.md', 'LICENSE', '{bin,lib}/**/*', 'dbf.gemspec'] s.require_paths = ['lib'] s.required_ruby_version = '>= 3.3.0' s.metadata['rubygems_mfa_required'] = 'true' s.metadata['source_code_uri'] = 'https://github.com/infused/dbf' s.metadata['changelog_uri'] = 'https://github.com/infused/dbf/blob/main/CHANGELOG.md' s.add_dependency 'csv' end dbf-5.2.0/lib/0000755000004100000410000000000015166514103013051 5ustar www-datawww-datadbf-5.2.0/lib/dbf/0000755000004100000410000000000015166514103013604 5ustar www-datawww-datadbf-5.2.0/lib/dbf/database/0000755000004100000410000000000015166514103015350 5ustar www-datawww-datadbf-5.2.0/lib/dbf/database/foxpro.rb0000644000004100000410000000675515166514103017227 0ustar www-datawww-data# frozen_string_literal: true module DBF # DBF::Database::Foxpro is the primary interface to a Visual Foxpro database # container (.dbc file). When using this database container, long fieldnames # are supported, and you can reference tables directly instead of # instantiating Table objects yourself. # Table references are created based on the filename, but it this class # tries to correct the table filenames because they could be wrong for # case sensitive filesystems, e.g. when a foxpro database is uploaded to # a linux server. module Database class Foxpro # Opens a DBF::Database::Foxpro # Examples: # # working with a database stored on the filesystem # db = DBF::Database::Foxpro.new 'path_to_db/database.dbc' # # # Calling a table # contacts = db.contacts.record(0) # # @param path [String] def initialize(path) @path = path @dirname = File.dirname(@path) @db = DBF::Table.new(@path) @tables = extract_dbc_data rescue Errno::ENOENT raise DBF::FileNotFoundError, "file not found: #{path}" end def table_names @tables.keys end # Returns table with given name # # @param name [String] # @return [DBF::Table] def table(name) Table.new(table_path(name), long_names: @tables[name]) end # Searches the database directory for the table's dbf file # and returns the absolute path. Ensures case-insensitivity # on any platform. # @param name [String] # @return [String] def table_path(name) glob = File.join(@dirname, "#{name}.dbf") path = Dir.glob(glob, File::FNM_CASEFOLD).first raise DBF::FileNotFoundError, "related table not found: #{name}" unless path && File.exist?(path) path end def method_missing(method, *args) # :nodoc: name = method.to_s table_names.index(name) ? table(name) : super end def respond_to_missing?(method, *) table_names.index(method.to_s) || super end private # This method extracts the data from the database container. This is # just an ordinary table with a treelike structure. Field definitions # are in the same order as in the linked tables but only the long name # is provided. def extract_dbc_data # :nodoc: build_table_data.values.to_h { |entry| entry.values_at(:name, :fields) } end def build_table_data # :nodoc: @db.each_with_object({}) do |record, hash| next unless record name = record.objectname case record.objecttype when 'Table' then hash[record.objectid] = table_field_hash(name) when 'Field' then (hash[record.parentid] ||= table_field_hash('UNKNOWN'))[:fields] << name end end end def table_field_hash(name) {name:, fields: []} end end class Table < DBF::Table attr_reader :long_names def initialize(path, long_names:) @long_names = long_names super(path) end def build_columns # :nodoc: columns = super # modify the column definitions to use the long names as the # columnname property is readonly, recreate the column definitions columns.map do |column| long_name = long_names[columns.index(column)] Column.new(self, long_name, column.type, column.length, column.decimal) end end end end end dbf-5.2.0/lib/dbf/column_builder.rb0000644000004100000410000000120415166514103017131 0ustar www-datawww-data# frozen_string_literal: true module DBF class ColumnBuilder def initialize(table, data, version_config) @table = table @data = data @version_config = version_config end def build safe_seek do @data.seek(@version_config.header_size) [].tap do |columns| columns << Column.new(*@version_config.read_column_args(@table, @data)) until end_of_record? end end end private def end_of_record? safe_seek { @data.read(1).ord == 13 } end def safe_seek original_pos = @data.pos yield.tap { @data.seek(original_pos) } end end end dbf-5.2.0/lib/dbf/header.rb0000644000004100000410000000111315166514103015355 0ustar www-datawww-data# frozen_string_literal: true module DBF class Header attr_reader :version, :record_count, :header_length, :record_length, :encoding_key, :encoding def initialize(data) @version = data.unpack1('H2') @encoding_key = nil @encoding = nil case @version when '02' @record_count, @record_length = data.unpack('x v x3 v') @header_length = 521 else @record_count, @header_length, @record_length, @encoding_key = data.unpack('x x3 V v2 x17 H2') @encoding = DBF::ENCODINGS[@encoding_key] end end end end dbf-5.2.0/lib/dbf/schema.rb0000644000004100000410000000702315166514103015373 0ustar www-datawww-data# frozen_string_literal: true module DBF # The Schema module is mixin for the Table class module Schema FORMATS = [:activerecord, :json, :sequel].freeze OTHER_DATA_TYPES = { 'Y' => ':decimal, :precision => 15, :scale => 4', 'D' => ':date', 'T' => ':datetime', 'L' => ':boolean', 'M' => ':text', 'B' => ':binary' }.freeze STRING_DATA_FORMATS = { sequel: ':varchar, :size => %s', activerecord: ':string, :limit => %s' }.freeze # 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 # # @param format [Symbol] format Valid options are :activerecord and :json # @param table_only [Boolean] # @return [String] def schema(format = :activerecord, table_only: false) schema_method_name = schema_name(format) send(schema_method_name, table_only: table_only) rescue NameError raise ArgumentError, ":#{format} is not a valid schema. Valid schemas are: #{FORMATS.join(', ')}." end def schema_name(format) # :nodoc: "#{format}_schema" end def activerecord_schema(*) # :nodoc: output = +"ActiveRecord::Schema.define do\n" output << " create_table \"#{name}\" do |t|\n" columns.each do |column| output << " t.column #{activerecord_schema_definition(column)}" end output << " end\nend" output end def sequel_schema(table_only: false) # :nodoc: output = +'' output << "Sequel.migration do\n change do\n " unless table_only output << " create_table(:#{name}) do\n" columns.each do |column| output << " column #{sequel_schema_definition(column)}" end output << " end\n" output << " end\nend\n" unless table_only output end def json_schema(*) # :nodoc: columns.map(&:to_hash).to_json end # ActiveRecord schema definition # # @param column [DBF::Column] # @return [String] def activerecord_schema_definition(column) "\"#{column.underscored_name}\", #{schema_data_type(column, :activerecord)}\n" end # Sequel schema definition # # @param column [DBF::Column] # @return [String] def sequel_schema_definition(column) ":#{column.underscored_name}, #{schema_data_type(column, :sequel)}\n" end def schema_data_type(column, format = :activerecord) # :nodoc: col_type = column.type case col_type when 'N', 'F', 'I' number_data_type(column) when 'Y', 'D', 'T', 'L', 'M', 'B' OTHER_DATA_TYPES[col_type] else string_data_format(format, column) end end def number_data_type(column) column.decimal > 0 ? ':float' : ':integer' end def string_data_format(format, column) STRING_DATA_FORMATS.fetch(format, STRING_DATA_FORMATS[:activerecord]) % column.length end end end dbf-5.2.0/lib/dbf/record_iterator.rb0000644000004100000410000000132415166514103017320 0ustar www-datawww-data# frozen_string_literal: true module DBF class RecordIterator def initialize(data, context, header_length, record_length, record_count) @data = data @context = context @header_length = header_length @record_length = record_length @record_count = record_count end def each buf = read_buffer return unless buf pos = 0 @record_count.times do if buf.getbyte(pos) == 0x2A yield nil else yield Record.new(buf, @context, pos + 1) end pos += @record_length end end private def read_buffer @data.seek(@header_length) @data.read(@record_length * @record_count) end end end dbf-5.2.0/lib/dbf/column.rb0000644000004100000410000000476715166514103015444 0ustar www-datawww-data# frozen_string_literal: true module DBF class Column class LengthError < StandardError end class NameError < StandardError end attr_reader :name, :type, :length, :decimal # rubocop:disable Style/MutableConstant TYPE_CAST_CLASS = { N: ColumnType::Number, I: ColumnType::SignedLong, F: ColumnType::Float, Y: ColumnType::Currency, D: ColumnType::Date, T: ColumnType::DateTime, L: ColumnType::Boolean, M: ColumnType::Memo, B: ColumnType::Double, G: ColumnType::General, :+ => ColumnType::AutoIncrement } # rubocop:enable Style/MutableConstant TYPE_CAST_CLASS.default = ColumnType::String TYPE_CAST_CLASS.freeze # Initialize a new DBF::Column # # @param table [String] # @param name [String] # @param type [String] # @param length [Integer] # @param decimal [Integer] def initialize(table, name, type, length, decimal) @table = table @name = clean(name) @type = type @length = length @decimal = decimal validate_length validate_name end def encoding = @table.encoding # @param value [String] def type_cast(value) type_cast_class.type_cast(value) end # Decodes a raw column value, handling memo, blank, and type cast cases # # @param raw [String] # @yield [raw] for memo column resolution # @return decoded value def decode(raw, &memo_handler) type_cast_class.decode(raw, &memo_handler) end # Returns a Hash with :name, :type, :length, and :decimal keys # # @return [Hash] def to_hash {name:, type:, length:, decimal:} end # Underscored name # # This is the column name converted to underscore format. # For example, MyColumn will be returned as my_column. # # @return [String] def underscored_name @underscored_name ||= name.gsub(/([a-z\d])([A-Z])/, '\1_\2').tr('-', '_').downcase end private def clean(value) # :nodoc: @table.encode_string(value.strip.split("\x00", 2).first || +'') end def type_cast_class # :nodoc: @type_cast_class ||= begin klass = @length == 0 ? ColumnType::Nil : TYPE_CAST_CLASS[type.to_sym] klass.new(self) end end def validate_length # :nodoc: raise LengthError, 'field length must be 0 or greater' if length < 0 end def validate_name # :nodoc: raise NameError, 'column name cannot be empty' if @name.empty? end end end dbf-5.2.0/lib/dbf/encodings.rb0000644000004100000410000000516415166514103016110 0ustar www-datawww-data# frozen_string_literal: true module DBF ENCODINGS = { '01' => 'cp437', # U.S. MS-DOS '02' => 'cp850', # International MS-DOS '03' => 'cp1252', # Windows ANSI '08' => 'cp865', # Danish OEM '09' => 'cp437', # Dutch OEM '0a' => 'cp850', # Dutch OEM* '0b' => 'cp437', # Finnish OEM '0d' => 'cp437', # French OEM '0e' => 'cp850', # French OEM* '0f' => 'cp437', # German OEM '10' => 'cp850', # German OEM* '11' => 'cp437', # Italian OEM '12' => 'cp850', # Italian OEM* '13' => 'cp932', # Japanese Shift-JIS '14' => 'cp850', # Spanish OEM* '15' => 'cp437', # Swedish OEM '16' => 'cp850', # Swedish OEM* '17' => 'cp865', # Norwegian OEM '18' => 'cp437', # Spanish OEM '19' => 'cp437', # English OEM (Britain) '1a' => 'cp850', # English OEM (Britain)* '1b' => 'cp437', # English OEM (U.S.) '1c' => 'cp863', # French OEM (Canada) '1d' => 'cp850', # French OEM* '1f' => 'cp852', # Czech OEM '22' => 'cp852', # Hungarian OEM '23' => 'cp852', # Polish OEM '24' => 'cp860', # Portuguese OEM '25' => 'cp850', # Portuguese OEM* '26' => 'cp866', # Russian OEM '37' => 'cp850', # English OEM (U.S.)* '40' => 'cp852', # Romanian OEM '4d' => 'cp936', # Chinese GBK (PRC) '4e' => 'cp949', # Korean (ANSI/OEM) '4f' => 'cp950', # Chinese Big5 (Taiwan) '50' => 'cp874', # Thai (ANSI/OEM) '57' => 'cp1252', # ANSI '58' => 'cp1252', # Western European ANSI '59' => 'cp1252', # Spanish ANSI '64' => 'cp852', # Eastern European MS-DOS '65' => 'cp866', # Russian MS-DOS '66' => 'cp865', # Nordic MS-DOS '67' => 'cp861', # Icelandic MS-DOS '6a' => 'cp737', # Greek MS-DOS (437G) '6b' => 'cp857', # Turkish MS-DOS '6c' => 'cp863', # French-Canadian MS-DOS '78' => 'cp950', # Taiwan Big 5 '79' => 'cp949', # Hangul (Wansung) '7a' => 'cp936', # PRC GBK '7b' => 'cp932', # Japanese Shift-JIS '7c' => 'cp874', # Thai Windows/MS-DOS '86' => 'cp737', # Greek OEM '87' => 'cp852', # Slovenian OEM '88' => 'cp857', # Turkish OEM 'c8' => 'cp1250', # Eastern European Windows 'c9' => 'cp1251', # Russian Windows 'ca' => 'cp1254', # Turkish Windows 'cb' => 'cp1253', # Greek Windows 'cc' => 'cp1257' # Baltic Windows }.freeze end dbf-5.2.0/lib/dbf/find.rb0000644000004100000410000000317715166514103015061 0ustar www-datawww-data# frozen_string_literal: true module DBF # The Find module provides methods for searching and retrieving # 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 command may be a record index, :all, or :first. # options 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 for the record to be # returned. The equivalent SQL would be "WHERE key1 = 'value1' # AND key2 = 'value2'". module Find # @param command [Integer, Symbol] command # @param options [optional, Hash] options Hash of search parameters # @yield [optional, DBF::Record, NilClass] def find(command, options = {}, &) case command when Integer then record(command) when Array then command.map { |index| record(index) } when :all then find_all_records(options, &) when :first then find_first_record(options) end end private def find_all_records(options) select do |record| next unless record&.match?(options) yield record if block_given? record end end def find_first_record(options) detect { |record| record&.match?(options) } end end end dbf-5.2.0/lib/dbf/file_handler.rb0000644000004100000410000000156315166514103016552 0ustar www-datawww-data# frozen_string_literal: true module DBF module FileHandler module_function def open_data(data) case data when StringIO data when String File.open(data, 'rb') else raise ArgumentError, 'data must be a file path or StringIO object' end rescue Errno::ENOENT raise DBF::FileNotFoundError, "file not found: #{data}" end def open_memo(data, memo, memo_class, version) if memo meth = memo.is_a?(StringIO) ? :new : :open memo_class.send(meth, memo, version) elsif !data.is_a?(StringIO) path = Dir.glob(memo_search_path(data)).first path && memo_class.open(path, version) end end def memo_search_path(io) dirname = File.dirname(io) basename = File.basename(io, '.*') "#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}" end end end dbf-5.2.0/lib/dbf/record_context.rb0000644000004100000410000000021315166514103017147 0ustar www-datawww-data# frozen_string_literal: true module DBF RecordContext = Struct.new(:columns, :version, :memo, :column_offsets, keyword_init: true) end dbf-5.2.0/lib/dbf/version_config.rb0000644000004100000410000000377515166514103017157 0ustar www-datawww-data# frozen_string_literal: true module DBF class VersionConfig DBASE2_HEADER_SIZE = 8 DBASE3_HEADER_SIZE = 32 DBASE7_HEADER_SIZE = 68 VERSIONS = { '02' => 'FoxBase', '03' => 'dBase III without memo file', '04' => 'dBase IV without memo file', '05' => 'dBase V without memo file', '07' => 'Visual Objects 1.x', '30' => 'Visual FoxPro', '32' => 'Visual FoxPro with field type Varchar or Varbinary', '31' => 'Visual FoxPro with AutoIncrement field', '43' => 'dBASE IV SQL table files, no memo', '63' => 'dBASE IV SQL system files, no memo', '7b' => 'dBase IV with memo file', '83' => 'dBase III with memo file', '87' => 'Visual Objects 1.x with memo file', '8b' => 'dBase IV with memo file', '8c' => 'dBase 7', '8e' => 'dBase IV with SQL table', 'cb' => 'dBASE IV SQL table files, with memo', 'f5' => 'FoxPro with memo file', 'fb' => 'FoxPro without memo file' }.freeze FOXPRO_VERSIONS = { '30' => 'Visual FoxPro', '31' => 'Visual FoxPro with AutoIncrement field', 'f5' => 'FoxPro with memo file', 'fb' => 'FoxPro without memo file' }.freeze attr_reader :version def initialize(version) @version = version end def version_description VERSIONS[version] end def header_size case version when '02' DBASE2_HEADER_SIZE when '04', '8c' DBASE7_HEADER_SIZE else DBASE3_HEADER_SIZE end end def foxpro? FOXPRO_VERSIONS.key?(version) end def memo_class if foxpro? Memo::Foxpro else version == '83' ? Memo::Dbase3 : Memo::Dbase4 end end def read_column_args(table, io) case version when '02' then [table, *io.read(header_size * 2).unpack('A11 a C'), 0] when '04', '8c' then [table, *io.read(48).unpack('A32 a C C x13')] else [table, *io.read(header_size).unpack('A11 a x4 C2')] end end end end dbf-5.2.0/lib/dbf/table.rb0000644000004100000410000001274715166514103015233 0ustar www-datawww-data# frozen_string_literal: true module DBF class FileNotFoundError < StandardError end class NoColumnsDefined < StandardError end # DBF::Table is the primary interface to a single DBF file and provides # methods for enumerating and searching the records. class Table extend Forwardable include Enumerable include ::DBF::Schema include ::DBF::Find attr_reader :encoding def_delegator :header, :header_length def_delegator :header, :record_count def_delegator :header, :record_length def_delegator :header, :version # Opens a DBF::Table # Examples: # # working with a file stored on the filesystem # table = DBF::Table.new 'data.dbf' # # # working with a misnamed memo file # table = DBF::Table.new 'data.dbf', 'memo.dbt' # # # working with a dbf in memory # table = DBF::Table.new StringIO.new(dbf_data) # # # working with a dbf and memo in memory # table = DBF::Table.new StringIO.new(dbf_data), StringIO.new(memo_data) # # # working with a dbf overriding specified in the dbf encoding # table = DBF::Table.new 'data.dbf', nil, 'cp437' # table = DBF::Table.new 'data.dbf', 'memo.dbt', Encoding::US_ASCII # # @param data [String, StringIO] data Path to the dbf file or a StringIO object # @param memo [optional String, StringIO] memo Path to the memo file or a StringIO object # @param encoding [optional String, Encoding] encoding Name of the encoding or an Encoding object def initialize(data, memo = nil, encoding = nil, name: nil) @data = FileHandler.open_data(data) @user_encoding = encoding @encoding = determine_encoding @memo = FileHandler.open_memo(data, memo, version_config.memo_class, version) @name = name yield self if block_given? end # Closes the table and memo file # # @return [TrueClass, FalseClass] def close @data.close @memo&.close end # @return [TrueClass, FalseClass] def closed? @data.closed? && (!@memo || @memo.closed?) end # Column names # # @return [String] def column_names @column_names ||= columns.map(&:name) end # Cumulative byte offsets for each column within a record # # @return [Array] def column_offsets @column_offsets ||= begin sum = 0 columns.map { |col| sum.tap { sum += col.length } } end end def record_context @record_context ||= RecordContext.new(columns:, version:, memo: @memo, column_offsets:) end # All columns # # @return [Array] def columns @columns ||= build_columns 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(&) return enum_for(:each) unless block_given? return if columns.empty? RecordIterator.new(@data, record_context, header_length, record_length, record_count).each(&) end # @return [String] def filename File.basename(@data.path) if @data.is_a?(File) end # @return [TrueClass, FalseClass] def has_memo_file? !!@memo end # @return [String] def name @name ||= filename && File.basename(filename, '.*') 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 [Integer] index # @return [DBF::Record, NilClass] def record(index) raise DBF::NoColumnsDefined, 'The DBF file has no columns defined' if columns.empty? seek_to_record(index) return nil if deleted_record? record_data = @data.read(record_length) DBF::Record.new(record_data, record_context) end alias row record # 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) csv = CSV.new(path ? File.open(path, 'w') : $stdout, force_quotes: true) csv << column_names each { |record| csv << record.to_a } end # Human readable version description # # @return [String] def version_description version_config.version_description end # Encode string # # @param [String] string # @return [String] def encode_string(string) # :nodoc: string.force_encoding(@encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace) end # Encoding specified in the file header # # @return [Encoding] def header_encoding header.encoding end private def version_config @version_config ||= VersionConfig.new(version) end def determine_encoding @user_encoding || header.encoding || Encoding.default_external end def build_columns # :nodoc: ColumnBuilder.new(self, @data, version_config).build end def deleted_record? # :nodoc: flag = @data.read(1) flag ? flag.getbyte(0) == 0x2A : true end def header # :nodoc: @header ||= safe_seek do @data.seek(0) Header.new(@data.read(VersionConfig::DBASE3_HEADER_SIZE)) end end def safe_seek # :nodoc: original_pos = @data.pos yield.tap { @data.seek(original_pos) } end def seek(offset) # :nodoc: @data.seek(header_length + offset) end def seek_to_record(index) # :nodoc: seek(index * record_length) end end end dbf-5.2.0/lib/dbf/memo/0000755000004100000410000000000015166514103014541 5ustar www-datawww-datadbf-5.2.0/lib/dbf/memo/dbase3.rb0000644000004100000410000000062215166514103016227 0ustar www-datawww-data# frozen_string_literal: true module DBF module Memo class Dbase3 < Base def build_memo(start_block) # :nodoc: data.seek offset(start_block) memo_string = +'' loop do block = data.read(BLOCK_SIZE).gsub(/(\000|\032)/, '') memo_string << block break if block.size < BLOCK_SIZE end memo_string end end end end dbf-5.2.0/lib/dbf/memo/dbase4.rb0000644000004100000410000000037515166514103016235 0ustar www-datawww-data# frozen_string_literal: true module DBF module Memo class Dbase4 < Base def build_memo(start_block) # :nodoc: data.seek offset(start_block) data.read(data.read(BLOCK_HEADER_SIZE).unpack1('x4L')) end end end end dbf-5.2.0/lib/dbf/memo/base.rb0000644000004100000410000000170615166514103016004 0ustar www-datawww-data# frozen_string_literal: true module DBF module Memo class Base BLOCK_HEADER_SIZE = 8 BLOCK_SIZE = 512 def self.open(filename, version) new(File.open(filename, 'rb'), version) end def initialize(data, version) @data = data @version = version end def get(start_block) return nil unless start_block > 0 build_memo start_block end def close @data.close && @data.closed? end def closed? @data.closed? end private attr_reader :data def offset(start_block) # :nodoc: start_block * block_size end def content_size(memo_size) # :nodoc: (memo_size - block_size) + BLOCK_HEADER_SIZE end def block_content_size # :nodoc: @block_content_size ||= block_size - BLOCK_HEADER_SIZE end def block_size # :nodoc: BLOCK_SIZE end end end end dbf-5.2.0/lib/dbf/memo/foxpro.rb0000644000004100000410000000166515166514103016413 0ustar www-datawww-data# frozen_string_literal: true module DBF module Memo class Foxpro < Base FPT_HEADER_SIZE = 512 def initialize(data, version) @data = data super end def build_memo(start_block) # :nodoc: @data.seek offset(start_block) memo_type, memo_size, memo_string = @data.read(block_size).unpack('NNa*') return nil unless memo_type == 1 && memo_size > 0 read_memo_content(memo_string, memo_size) rescue StandardError nil end private def read_memo_content(memo_string, memo_size) # :nodoc: if memo_size > block_content_size memo_string << @data.read(content_size(memo_size)) else memo_string[0, memo_size] end end def block_size # :nodoc: @block_size ||= begin @data.rewind @data.read(FPT_HEADER_SIZE).unpack1('x6n') || 0 end end end end end dbf-5.2.0/lib/dbf/record.rb0000644000004100000410000000640115166514103015410 0ustar www-datawww-data# frozen_string_literal: true module DBF # An instance of DBF::Record represents a row in the DBF file class Record # Initialize a new DBF::Record # # @param data [String, StringIO] data # @param context [DBF::RecordContext] # @param offset [Integer] def initialize(data, context, offset = 0) @data = data @context = context @offset = offset @to_a = nil end # Equality # # @param [DBF::Record] other # @return [Boolean] def ==(other) attributes == other.attributes rescue NoMethodError false end # Reads attributes by column name # # @param name [String, Symbol] key def [](name) key = name.to_s if @context.column_offsets && !@to_a index = column_name_index(key) index ? column_value(index) : nil elsif attributes.key?(key) attributes[key] elsif (index = underscored_column_names.index(key)) attributes[@context.columns[index].name] end end # Record attributes # # @return [Hash] def attributes @attributes ||= column_names.zip(to_a).to_h end # Do all search parameters match? # # @param [Hash] options # @return [Boolean] def match?(options) options.all? { |key, value| self[key] == value } end # Maps a row to an array of values # # @return [Array] def to_a @to_a ||= begin data = @data offset = @offset columns = @context.columns col_count = columns.length result = Array.new(col_count) index = 0 while index < col_count column = columns[index] len = column.length raw = data.byteslice(offset, len) offset += len result[index] = decode_column(raw, column) index += 1 end @offset = offset result end end private def decode_memo_value(raw) # :nodoc: memo = @context.memo return nil unless memo version = @context.version raw = raw.unpack1('V') if version == '30' || version == '31' memo.get(raw.to_i) end def column_name_index(key) # :nodoc: column_names.index(key) || underscored_column_names.index(key) end def column_value(index) # :nodoc: column = @context.columns[index] col_offset = @offset + @context.column_offsets[index] len = column.length raw = @data.byteslice(col_offset, len) decode_column(raw, column) end def decode_column(raw, column) # :nodoc: column.decode(raw) { |raw_memo| decode_memo_value(raw_memo) } end def column_names # :nodoc: @column_names ||= @context.columns.map(&:name) end def method_missing(method, *args) # :nodoc: key = method.to_s if (index = underscored_column_names.index(key)) if @context.column_offsets && !@to_a column_value(index) else attributes[@context.columns[index].name] end else super end end def respond_to_missing?(method, *) # :nodoc: underscored_column_names.include?(method.to_s) || super end def underscored_column_names # :nodoc: @underscored_column_names ||= @context.columns.map(&:underscored_name) end end end dbf-5.2.0/lib/dbf/version.rb0000644000004100000410000000010215166514103015607 0ustar www-datawww-data# frozen_string_literal: true module DBF VERSION = '5.2.0' end dbf-5.2.0/lib/dbf/column_type.rb0000644000004100000410000000710515166514103016472 0ustar www-datawww-data# frozen_string_literal: true module DBF module ColumnType class Base attr_reader :decimal, :encoding # @param decimal [Integer] # @param encoding [String, Encoding] def initialize(column) @decimal = column.decimal @encoding = column.encoding end def blank_value nil end def skip_blank? false end def decode(raw, &_memo_handler) if skip_blank? && raw.count(' ') == raw.length blank_value else type_cast(raw) end end end class Nil < Base # @param _value [String] def type_cast(_value) nil end end class Number < Base def skip_blank? = true # @param value [String] def type_cast(value) return nil if value.empty? decimal.zero? ? value.to_i : value.to_f end end class Currency < Base # @param value [String] def type_cast(value) (value.unpack1('q<') / 10_000.0).to_f end end class SignedLong < Base # @param value [String] def type_cast(value) value.unpack1('l<') end end class AutoIncrement < Base # @param value [String] def type_cast(value) bits = value.unpack1('B*') sign_multiplier = bits[0] == '0' ? -1 : 1 bits[1, 31].to_i(2) * sign_multiplier end end class Float < Base # @param value [String] def type_cast(value) value.to_f end end class Double < Base # @param value [String] def type_cast(value) value.unpack1('E') end end class Boolean < Base def skip_blank? = true def blank_value = false # @param value [String] def type_cast(value) byte = value.getbyte(0) byte == 89 || byte == 121 || byte == 84 || byte == 116 # Y y T t end end class Date < Base def skip_blank? = true def blank_value = false # @param value [String] def type_cast(value) value.match?(/\d{8}/) && ::Date.strptime(value, '%Y%m%d') rescue StandardError nil end end class DateTime < Base # @param value [String] def type_cast(value) days, msecs = value.unpack('l2') secs = (msecs / 1000).to_i ::DateTime.jd(days, (secs / 3600).to_i, (secs / 60).to_i % 60, secs % 60).to_time rescue StandardError nil end end class Memo < Base def decode(raw, &memo_handler) memo_content = memo_handler.call(raw) memo_content ? type_cast(memo_content) : nil end # @param value [String] def type_cast(value) return value unless encoding && value value.dup.force_encoding(encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace) end end class General < Base # @param value [String] def type_cast(value) value&.dup&.force_encoding(Encoding::ASCII_8BIT) end end class String < Base def initialize(column) super @target_encoding = Encoding.default_external @needs_encode = encoding && encoding != @target_encoding end def skip_blank? = true def blank_value = '' # @param value [String] def type_cast(value) value.strip! encoding ? encode(value) : value end private def encode(value) value.force_encoding(encoding) @needs_encode ? value.encode(@target_encoding, undef: :replace, invalid: :replace) : value end end end end dbf-5.2.0/lib/dbf.rb0000644000004100000410000000111015166514103014122 0ustar www-datawww-data# frozen_string_literal: true require 'csv' require 'date' require 'forwardable' require 'json' require 'time' require 'dbf/version' require 'dbf/schema' require 'dbf/find' require 'dbf/record' require 'dbf/record_context' require 'dbf/column_type' require 'dbf/column' require 'dbf/encodings' require 'dbf/header' require 'dbf/version_config' require 'dbf/file_handler' require 'dbf/column_builder' require 'dbf/record_iterator' require 'dbf/table' require 'dbf/memo/base' require 'dbf/memo/dbase3' require 'dbf/memo/dbase4' require 'dbf/memo/foxpro' require 'dbf/database/foxpro' dbf-5.2.0/LICENSE0000644000004100000410000000207415166514103013313 0ustar www-datawww-dataCopyright (c) 2006-2024 Keith Morrison Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dbf-5.2.0/README.md0000644000004100000410000003063115166514103013565 0ustar www-datawww-data# DBF [![Version](https://img.shields.io/gem/v/dbf.svg?style=flat)](https://rubygems.org/gems/dbf) [![Build Status](https://github.com/infused/dbf/actions/workflows/build.yml/badge.svg)](https://github.com/infused/dbf/actions/workflows/build.yml) [![Code Quality](https://img.shields.io/codeclimate/maintainability/infused/dbf.svg?style=flat)](https://codeclimate.com/github/infused/dbf) [![Code Coverage](https://img.shields.io/codeclimate/c/infused/dbf.svg?style=flat)](https://codeclimate.com/github/infused/dbf) [![Total Downloads](https://img.shields.io/gem/dt/dbf.svg)](https://rubygems.org/gems/dbf/) [![License](https://img.shields.io/github/license/infused/dbf.svg)](https://github.com/infused/dbf) DBF is a small, fast Ruby library for reading dBase, xBase, Clipper, and FoxPro database files. * Project page: * API Documentation: * Report bugs: * Questions: Email and put DBF somewhere in the subject line * Change log: NOTE: Beginning with version 5.2 we have dropped support for Ruby 3.2 and earlier. NOTE: Beginning with version 4.3 we have dropped support for Ruby 3.0 and earlier. NOTE: Beginning with version 4 we have dropped support for Ruby 2.0, 2.1, 2.2, and 2.3. If you need support for these older Rubies, please use 3.0.x () NOTE: Beginning with version 3 we have dropped support for Ruby 1.8 and 1.9. If you need support for older Rubies, please use 2.0.x () ## Compatibility DBF is tested to work with the following versions of Ruby: * Ruby 3.3.x, 3.4.x, 4.0.x ## Installation Install the gem manually: ```ruby gem install dbf ``` Or add to your Gemfile: ```ruby gem 'dbf' ``` ## Basic Usage Open a DBF file using a path: ```ruby require 'dbf' widgets = DBF::Table.new("widgets.dbf") ``` Open a DBF file using an IO object: ```ruby data = File.open('widgets.dbf') widgets = DBF::Table.new(data) ``` Open a DBF by passing in raw data (wrap the raw data with StringIO): ```ruby widgets = DBF::Table.new(StringIO.new('raw binary data')) ``` Enumerate all records ```ruby widgets.each do |record| puts record.name puts record.email end ``` Find a single record ```ruby widget = widgets.find(6) ``` Note that find() will return nil if the requested record has been deleted and not yet pruned from the database. The value for an attribute can be accessed via element reference in several ways. ```ruby widget.slot_number # underscored field name as method widget["SlotNumber"] # original field name in dbf file widget['slot_number'] # underscored field name string widget[:slot_number] # underscored field name symbol ``` Get a hash of all attributes. The keys are the original column names. ```ruby widget.attributes # => {"Name" => "Thing1 | SlotNumber" => 1} ``` Search for records using a simple hash format. Multiple search criteria are ANDed. Use the block form if the resulting record set is too big. Otherwise, all records are loaded into memory. ```ruby # find all records with slot_number equal to s42 widgets.find(:all, slot_number: 's42') do |widget| # the record will be nil if deleted, but not yet pruned from the database if widget puts widget.serial_number end end # find the first record with slot_number equal to s42 widgets.find :first, slot_number: 's42' # find record number 10 widgets.find(10) ``` ## Enumeration DBF::Table is a Ruby Enumerable, so you get several traversal, search, and sort methods for free. For example, let's get only records created before January 1st, 2015: ```ruby widgets.select { |w| w.created_date < Date.new(2015, 1, 1) } ``` Or custom sorting: ```ruby widgets.sort_by { |w| w.created_date } ``` ## Encodings (Code Pages) dBase supports encoding non-english characters with different character sets. Unfortunately, the character set used may not be set explicitly. In that case, you will have to specify it manually. For example, if you know the dbf file is encoded with 'Russian OEM': ```ruby table = DBF::Table.new('dbf/books.dbf', nil, 'cp866') ``` | Code Page | Encoding | Description | | --------- | -------- | ----------- | | 01 | cp437 | U.S. MS–DOS | | 02 | cp850 | International MS–DOS | | 03 | cp1252 | Windows ANSI | | 08 | cp865 | Danish OEM | | 09 | cp437 | Dutch OEM | | 0a | cp850 | Dutch OEM* | | 0b | cp437 | Finnish OEM | | 0d | cp437 | French OEM | | 0e | cp850 | French OEM* | | 0f | cp437 | German OEM | | 10 | cp850 | German OEM* | | 11 | cp437 | Italian OEM | | 12 | cp850 | Italian OEM* | | 13 | cp932 | Japanese Shift-JIS | | 14 | cp850 | Spanish OEM* | | 15 | cp437 | Swedish OEM | | 16 | cp850 | Swedish OEM* | | 17 | cp865 | Norwegian OEM | | 18 | cp437 | Spanish OEM | | 19 | cp437 | English OEM (Britain) | | 1a | cp850 | English OEM (Britain)* | | 1b | cp437 | English OEM (U.S.) | | 1c | cp863 | French OEM (Canada) | | 1d | cp850 | French OEM* | | 1f | cp852 | Czech OEM | | 22 | cp852 | Hungarian OEM | | 23 | cp852 | Polish OEM | | 24 | cp860 | Portuguese OEM | | 25 | cp850 | Portuguese OEM* | | 26 | cp866 | Russian OEM | | 37 | cp850 | English OEM (U.S.)* | | 40 | cp852 | Romanian OEM | | 4d | cp936 | Chinese GBK (PRC) | | 4e | cp949 | Korean (ANSI/OEM) | | 4f | cp950 | Chinese Big5 (Taiwan) | | 50 | cp874 | Thai (ANSI/OEM) | | 57 | cp1252 | ANSI | | 58 | cp1252 | Western European ANSI | | 59 | cp1252 | Spanish ANSI | | 64 | cp852 | Eastern European MS–DOS | | 65 | cp866 | Russian MS–DOS | | 66 | cp865 | Nordic MS–DOS | | 67 | cp861 | Icelandic MS–DOS | | 6a | cp737 | Greek MS–DOS (437G) | | 6b | cp857 | Turkish MS–DOS | | 6c | cp863 | French–Canadian MS–DOS | | 78 | cp950 | Taiwan Big 5 | | 79 | cp949 | Hangul (Wansung) | | 7a | cp936 | PRC GBK | | 7b | cp932 | Japanese Shift-JIS | | 7c | cp874 | Thai Windows/MS–DOS | | 86 | cp737 | Greek OEM | | 87 | cp852 | Slovenian OEM | | 88 | cp857 | Turkish OEM | | c8 | cp1250 | Eastern European Windows | | c9 | cp1251 | Russian Windows | | ca | cp1254 | Turkish Windows | | cb | cp1253 | Greek Windows | | cc | cp1257 | Baltic Windows | ## Migrating to ActiveRecord An example of migrating a DBF book table to ActiveRecord using a migration: ```ruby require 'dbf' class Book < ActiveRecord::Base; end class CreateBooks < ActiveRecord::Migration def self.up table = DBF::Table.new('db/dbf/books.dbf') eval(table.schema) Book.reset_column_information table.each do |record| Book.create(title: record.title, author: record.author) end end def self.down drop_table :books end end ``` If you have initialized the DBF::Table with raw data, you will need to set the exported table name manually: ```ruby table.name = 'my_table_name' ``` ## Migrating to Sequel An example of migrating a DBF book table to Sequel using a migration: ```ruby require 'dbf' class Book < Sequel::Model; end Sequel.migration do up do table = DBF::Table.new('db/dbf/books.dbf') eval(table.schema(:sequel, table_only: true)) # limit output to create_table() only Book.reset_column_information table.each do |record| Book.create(title: record.title, author: record.author) end end down do drop_table(:books) end end ``` If you have initialized the DBF::Table with raw data, you will need to set the exported table name manually: ```ruby table.name = 'my_table_name' ``` ## Command-line utility A small command-line utility called dbf is installed with the gem. $ dbf -h usage: dbf [-h|-s|-a] filename -h = print this message -v = print the version number -s = print summary information -a = create an ActiveRecord::Schema -r = create a Sequel Migration -c = export as CSV Create an executable ActiveRecord schema: dbf -a books.dbf > books_schema.rb Create an executable Sequel schema: dbf -r books.dbf > migrate/001_create_books.rb Dump all records to a CSV file: dbf -c books.dbf > books.csv ## Reading a Visual Foxpro database (v8, v9) A special Database::Foxpro class is available to read Visual Foxpro container files (file with .dbc extension). When using this class, long field names are supported, and tables can be referenced without using names. ```ruby require 'dbf' contacts = DBF::Database::Foxpro.new('contact_database.dbc').contacts my_contact = contacts.record(1).spouses_interests ``` ## dBase version compatibility The basic dBase data types are generally supported well. Support for the advanced data types in dBase V and FoxPro are still experimental or not supported. If you have insight into how any of the unsupported data types are implemented, please open an issue on Github. FoxBase/dBase II files are not supported at this time. ### Supported data types by dBase version | Version | Description | C | N | L | D | M | F | B | G | P | Y | T | I | V | X | @ | O | + | |---------|-----------------------------------------------------|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---| | 02 | FoxBase | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - | | 03 | dBase III without memo file | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - | | 04 | dBase IV without memo file | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - | | 05 | dBase V without memo file | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - | | 07 | Visual Objects 1.x | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - | | 30 | Visual FoxPro | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | N | Y | N | N | N | N | - | | 31 | Visual FoxPro with AutoIncrement | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | N | Y | N | N | N | N | N | | 32 | Visual FoxPro with field type Varchar or Varbinary | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | N | Y | N | N | N | N | N | | 7b | dBase IV with memo file | Y | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | | 83 | dBase III with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | | 87 | Visual Objects 1.x with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | | 8b | dBase IV with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | N | - | - | - | | 8e | dBase IV with SQL table | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | N | - | - | - | | f5 | FoxPro with memo file | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | N | Y | N | N | N | N | N | | fb | FoxPro without memo file | Y | Y | Y | Y | - | Y | Y | Y | N | Y | N | Y | N | N | N | N | N | Data type descriptions * C = Character * N = Number * L = Logical * D = Date * M = Memo * F = Float * B = Binary * G = General * P = Picture * Y = Currency * T = DateTime * I = Integer * V = VariField * X = SQL compat * @ = Timestamp * O = Double * + = Autoincrement ## Limitations * DBF is read-only * Index files are not utilized ## License Copyright (c) 2006-2024 Keith Morrison <> Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. dbf-5.2.0/CHANGELOG.md0000644000004100000410000001740415166514103014122 0ustar www-datawww-data# Changelog ## 5.2.0 - Drop support for Ruby 3.1 and 3.2 ## 5.1.1 - Frozen string literals ## 5.1.0 - Drop support for Ruby 3.0.x ## 5.0.1 - Raise ArgumentError data is not a string or StringIO object ## 5.0.0 - Refactor the Column class to support non-ASCII header names - Output encoding is now set to UTF-8 if there is no embedded encoding and one is not specified during DBF::Table initialization. ## 4.3.2 - Fixes to maintain support for Ruby 3.0.x until it's EOL ## 4.3.1 - Fix bug (since 4.2.0) that caused column names not to be truncated after null character ## 4.3.0 - Drop support for Ruby versions older than 3.0 - Require CSV gem ## 4.2.4 - Exclude unnecessary files from the gem file list ## 4.2.3 - Require MFA to publish gem ## 4.2.2 - Faster CSV generation ## 4.2.1 - Support for dBase IV "04" type files ## 4.2.0 - Initial support for dBase 7 files ## 4.1.6 - Add support for file type 32 ## 4.1.5 - Better handling for PIPE errors when using command line utility ## 4.1.4 - Add full support for FoxBase files ## 4.1.3 - Raise DBF::NoColumnsDefined error when attempting to read records if no columns are defined ## 4.1.1 - Add required_ruby_version to gemspec ## 4.1.0 - Return Time instead of DateTime ## 4.0.0 - Drop support for ruby-2.2 and earlier ## 3.1.3 - Ensure malformed dates return nil ## 3.1.2 - Fix incorrect columns list when StringIO and encoding set ## 3.1.1 - Use Date.strptime to parse date fields ## 3.1.0 - Use :binary for binary fields in ActiveRecord schemas ## 3.0.8 - Fix uninitialized constant error under Rails 5 ## 3.0.7 - Ignore non-existent records if header record count is incorrect ## 3.0.6 - This version has been yanked from rubygems due to errors ## 3.0.5 - Override table name for schema output ## 3.0.4 - Adds -v command-line option to print version - Adds -r command-line option to create Sequel migration ## 3.0.3 - Uninitialized (N)umbers should return nil ## 3.0.2 - Performance improvements for large files ## 3.0.1 - Support FoxPro (G) general field type - Fix ruby warnings ## 3.0.0 - Requires Ruby version 2.0 and above - Support the (G) General Foxpro field type ## 2.0.13 - Support 64-bit currency signed currency values (see https://github.com/infused/dbf/pull/71) ## 2.0.12 - Parse (I) values as signed (see https://github.com/infused/dbf/pull/70) ## 2.0.11 - Foxpro doubles should always return the full stored precision (see https://github.com/infused/dbf/pull/69) ## 2.0.10 - allow 0 length fields, but always return nil as value ## 2.0.9 - fix dBase IV attributes when memo file is missing ## 2.0.8 - fix FoxPro currency fields on some builds of Ruby 1.9.3 and 2.0.0 ## 2.0.7 - fix the dbf binary on some linux systems ## 2.0.6 - build_memo returns nil on errors ## 2.0.5 - use correct FoxPro memo block size ## 2.0.4 - memo fields return nil if memo file is missing ## 2.0.3 - set encoding if table encoding is nil ## 2.0.2 - Allow overriding the character encoding specified in the file ## 2.0.1 - Add experimental support for character encodings under Ruby 1.8 ## 2.0.0 - #44 Require FasterCSV gem on all platforms - Remove rdoc development dependency - #42 Fixes encoding of memos - #43 Improve handling of record attributes ## 1.7.5 - fixes FoxPro currency (Y) fields ## 1.7.4 - Replace Memo Type with Memo File boolean in command-line utility summary output ## 1.7.3 - find_all/find_first should ignore deleted records ## 1.7.2 - Fix integer division under Ruby 1.8 when requiring mathn standard library (see http://bugs.ruby-lang.org/issues/2121) ## 1.7.1 - Fix Table.FOXPRO_VERSIONS breakage on Ruby 1.8 ## 1.7.0 - allow DBF::Table to work with dbf data in memory - allow DBF::Table#to_csv to write to STDOUT ## 1.6.7 - memo columns return nil when no memo file found ## 1.6.6 - add binary data type support to ActiveRecord schema output ## 1.6.5 - support for visual foxpro double (b) data type ## 1.6.3 - Replace invalid chars with 'unicode replacement character' (U+FFFD) ## 1.6.2 - add Table#filename method - Rakefile now loads gems with bundler - add Table#supports_encoding? - simplify encodings.yml loader - add rake and rdoc as development dependencies - simplify open_memo file search logic - remove unnecessary requires in spec helper - fix cli summary ## 1.6.1 - fix YAML issue when using MRI version > 1.9.1 - remove Table#seek_to_index and Table#current_record private methods ## 1.6.0 - remove activesupport gem dependency ## 1.5.0 - Significant internal restructuring and performance improvements. Initial testing shows 4x faster performance. ## 1.3.0 - Only load what's needed from activesupport 3.0 - Updatate fastercsv dependency to 1.5.3 - Remove use of 'returning' method - Remove jeweler in favor of manual gemspec creation - Move Table#all_values_match? to Record#match? - Add attr_reader for Record#table - Use method_defined? instead of respond_to? when defining attribute accessors - Move memo file check into get_memo_header_info - Remove unnecessary seek_to_record in Table#each - Add rake console task - New Attribute class - Add a helper method for memo column type - Move constants into the classes where they are used - Use bundler ## 1.2.9 - Retain trailing whitespace in memos ## 1.2.8 - Handle missing zeros in date values [#11] ## 1.2.7 - MIT License ## 1.2.6 - Support for Ruby 1.9.2 ## 1.2.5 - Remove ruby warning switch - Requires activesupport version 2.3.5 ## 1.2.4 - Add csv output option to dbf command-line utility - Read Visual FoxPro memos ## 1.2.3 - Small performance gain when unpacking values from the dbf file - Correctly handle FoxPro's integer data type ## 1.2.2 - Handle invalid date fields ## 1.2.1 - Add support for F field type (Float) ## 1.2.0 - Add Table#to_a ## 1.1.1 - Return invalid DateTime columns as nil ## 1.1.0 - Add support for large table that will not fit into memory ## 1.0.13 - Allow passing an array of ids to find ## 1.0.11 - Attributes are now accessible by original or underscored name ## 1.0.9 - Fix incorrect integer column values (only affecting some dbf files) - Add CSV export ## 1.0.8 - Truncate column names on NULL - Fix schema dump for date and datetime columns - Replace internal helpers with ActiveSupport - Always underscore attribute names ## 1.0.7 - Remove support for original column names. All columns names are now downcased/underscored. ## 1.0.6 - DBF::Table now includes the Enumerable module - Return nil for memo values if the memo file is missing - Finder conditions now support the original and downcased/underscored column names ## 1.0.5 - Strip non-ascii characters from column names ## 1.0.4 - Underscore column names when dumping schemas (FieldId becomes field_id) ## 1.0.3 - Add support for Visual Foxpro Integer and Datetime columns ## 1.0.2 - Compatibility fix for Visual Foxpro memo files (ignore negative memo index values) ## 1.0.1 - Fixes error when using the command-line interface [#11984] ## 1.0.0 - Renamed classes and refactored code in preparation for adding the ability to save records and create/compact databases. - The Reader class has been renamed to Table - Attributes are no longer accessed directly from the record. Use record.attribute['column_name'] instead, or use the new attribute accessors detailed under Basic Usage. ## 0.5.4 - Ignore deleted records in both memory modes ## 0.5.3 - Added a standalone dbf utility (try dbf -h for help) ## 0.5.0 / 2007-05-25 - New find method - Full compatibility with the two flavors of memo file - Two modes of operation: - In memory (default): All records are loaded into memory on the first request. Records are retrieved from memory for all subsequent requests. - File I/O: All records are retrieved from disk on every request - Improved documentation and more usage examples