dbf-5.2.0/ 0000755 0000041 0000041 00000000000 15166514103 012303 5 ustar www-data www-data dbf-5.2.0/bin/ 0000755 0000041 0000041 00000000000 15166514103 013053 5 ustar www-data www-data dbf-5.2.0/bin/dbf 0000755 0000041 0000041 00000003016 15166514103 013534 0 ustar www-data www-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.gemspec 0000644 0000041 0000041 00000001453 15166514103 014406 0 ustar www-data www-data require_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/ 0000755 0000041 0000041 00000000000 15166514103 013051 5 ustar www-data www-data dbf-5.2.0/lib/dbf/ 0000755 0000041 0000041 00000000000 15166514103 013604 5 ustar www-data www-data dbf-5.2.0/lib/dbf/database/ 0000755 0000041 0000041 00000000000 15166514103 015350 5 ustar www-data www-data dbf-5.2.0/lib/dbf/database/foxpro.rb 0000644 0000041 0000041 00000006755 15166514103 017227 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000001204 15166514103 017131 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000001113 15166514103 015355 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000007023 15166514103 015373 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000001324 15166514103 017320 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000004767 15166514103 015444 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000005164 15166514103 016110 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000003177 15166514103 015061 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000001563 15166514103 016552 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000000213 15166514103 017147 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000003775 15166514103 017157 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000012747 15166514103 015233 0 ustar www-data www-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/ 0000755 0000041 0000041 00000000000 15166514103 014541 5 ustar www-data www-data dbf-5.2.0/lib/dbf/memo/dbase3.rb 0000644 0000041 0000041 00000000622 15166514103 016227 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000000375 15166514103 016235 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000001706 15166514103 016004 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000001665 15166514103 016413 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000006401 15166514103 015410 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000000102 15166514103 015607 0 ustar www-data www-data # frozen_string_literal: true
module DBF
VERSION = '5.2.0'
end
dbf-5.2.0/lib/dbf/column_type.rb 0000644 0000041 0000041 00000007105 15166514103 016472 0 ustar www-data www-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.rb 0000644 0000041 0000041 00000001110 15166514103 014122 0 ustar www-data www-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/LICENSE 0000644 0000041 0000041 00000002074 15166514103 013313 0 ustar www-data www-data 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/README.md 0000644 0000041 0000041 00000030631 15166514103 013565 0 ustar www-data www-data # DBF
[](https://rubygems.org/gems/dbf)
[](https://github.com/infused/dbf/actions/workflows/build.yml)
[](https://codeclimate.com/github/infused/dbf)
[](https://codeclimate.com/github/infused/dbf)
[](https://rubygems.org/gems/dbf/)
[](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.md 0000644 0000041 0000041 00000017404 15166514103 014122 0 ustar www-data www-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