dbf 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/dbf.rb +2 -0
- data/lib/dbf/reader.rb +116 -0
- data/test/databases/foxpro.dbf +0 -0
- data/test/databases/foxpro.fpt +0 -0
- data/test/read_test.rb +65 -0
- metadata +52 -0
data/lib/dbf.rb
ADDED
data/lib/dbf/reader.rb
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
module DBF
|
2
|
+
|
3
|
+
DBF_HEADER_SIZE = 32
|
4
|
+
FPT_HEADER_SIZE = 512
|
5
|
+
FPT_BLOCK_HEADER_SIZE = 8
|
6
|
+
|
7
|
+
class Reader
|
8
|
+
|
9
|
+
attr_reader :field_count
|
10
|
+
attr_reader :fields
|
11
|
+
attr_reader :record_count
|
12
|
+
|
13
|
+
def initialize(file)
|
14
|
+
@data_file = File.open(file, 'rb')
|
15
|
+
@memo_file = File.open(file.gsub(/dbf$/i, 'fpt'), 'rb') rescue File.open(file.gsub(/dbf$/i, 'FPT'), 'rb') rescue nil
|
16
|
+
get_header_info
|
17
|
+
get_memo_header_info if @memo_file
|
18
|
+
get_field_descriptors
|
19
|
+
end
|
20
|
+
|
21
|
+
def get_header_info
|
22
|
+
@data_file.rewind
|
23
|
+
@record_count, @header_length, @record_length = @data_file.read(DBF_HEADER_SIZE).unpack('xxxxVvv')
|
24
|
+
@field_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_memo_header_info
|
28
|
+
@memo_file.rewind
|
29
|
+
@memo_next_available_block, @memo_block_size = @memo_file.read(FPT_HEADER_SIZE).unpack('Nxxn')
|
30
|
+
end
|
31
|
+
|
32
|
+
def has_memo_file?
|
33
|
+
@memo_file ? true : false
|
34
|
+
end
|
35
|
+
|
36
|
+
def get_field_descriptors
|
37
|
+
@fields = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('a10xax4ss'))}
|
38
|
+
end
|
39
|
+
|
40
|
+
def field(field_name)
|
41
|
+
@fields.select {|f| f.name == field_name}
|
42
|
+
end
|
43
|
+
|
44
|
+
def memo(start_block)
|
45
|
+
@memo_file.rewind
|
46
|
+
@memo_file.seek(start_block * @memo_block_size)
|
47
|
+
memo_type, memo_size, memo_string = @memo_file.read(@memo_block_size).unpack("NNa56")
|
48
|
+
if memo_size > @memo_block_size - FPT_BLOCK_HEADER_SIZE
|
49
|
+
memo_string << @memo_file.read(memo_size - @memo_block_size + FPT_BLOCK_HEADER_SIZE)
|
50
|
+
end
|
51
|
+
memo_string
|
52
|
+
end
|
53
|
+
|
54
|
+
def unpack_string(field)
|
55
|
+
@data_file.read(field.length).unpack("a#{field.length}")
|
56
|
+
end
|
57
|
+
|
58
|
+
def records
|
59
|
+
seek(0)
|
60
|
+
Array.new(@record_count) {build_record if @data_file.read(1).unpack('c').to_s == '32'}
|
61
|
+
end
|
62
|
+
|
63
|
+
alias_method :rows, :records
|
64
|
+
|
65
|
+
def record(index)
|
66
|
+
seek_to_record(index)
|
67
|
+
build_record if @data_file.read(1).unpack('c').to_s == '32'
|
68
|
+
end
|
69
|
+
|
70
|
+
def build_record
|
71
|
+
record = Record.new
|
72
|
+
@fields.each do |field|
|
73
|
+
case field.type
|
74
|
+
when 'N' # number
|
75
|
+
record[field.name] = unpack_string(field)[0].to_i
|
76
|
+
when 'D' # date
|
77
|
+
raw = unpack_string(field).to_s.strip
|
78
|
+
record[field.name] = raw.strip.empty? ? nil : Date.new(*raw.match(/([\d]{4})([\d]{2})([\d]{2})/).to_a.slice(1,3).map {|n| n.to_i}) rescue nil
|
79
|
+
when 'M' # memo
|
80
|
+
starting_block = unpack_string(field).first.to_i
|
81
|
+
record[field.name] = starting_block == 0 ? nil : memo(starting_block)
|
82
|
+
when 'L' # logical
|
83
|
+
record[field.name] = unpack_string(field) =~ /(y|t)/i ? true : false
|
84
|
+
else
|
85
|
+
record[field.name] = unpack_string(field).to_s.strip
|
86
|
+
end
|
87
|
+
end
|
88
|
+
record
|
89
|
+
end
|
90
|
+
|
91
|
+
def seek(offset)
|
92
|
+
@data_file.seek(@header_length + offset)
|
93
|
+
end
|
94
|
+
|
95
|
+
def seek_to_record(index)
|
96
|
+
seek(@record_length * index)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
class Field
|
102
|
+
attr_accessor :name, :type, :length, :decimal
|
103
|
+
|
104
|
+
def initialize(name, type, length, decimal)
|
105
|
+
self.name, self.type, self.length, self.decimal = name, type, length, decimal
|
106
|
+
end
|
107
|
+
|
108
|
+
def name=(name)
|
109
|
+
@name = name.gsub(/\0/, '')
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
class Record < Hash
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
Binary file
|
Binary file
|
data/test/read_test.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib/")
|
2
|
+
require 'test/unit'
|
3
|
+
require 'dbf'
|
4
|
+
|
5
|
+
class ReadTest < Test::Unit::TestCase
|
6
|
+
|
7
|
+
def setup
|
8
|
+
@dbf = DBF::Reader.new(File.join(File.dirname(__FILE__),'databases', 'foxpro.dbf'))
|
9
|
+
end
|
10
|
+
|
11
|
+
def test_records
|
12
|
+
assert_kind_of Array, @dbf.records
|
13
|
+
assert_kind_of Array, @dbf.rows
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_record
|
17
|
+
assert_equal @dbf.record(0), @dbf.records[0]
|
18
|
+
assert_equal @dbf.record(99), @dbf.records[99]
|
19
|
+
end
|
20
|
+
|
21
|
+
def test_field_count
|
22
|
+
assert_equal @dbf.field_count, @dbf.fields.size
|
23
|
+
end
|
24
|
+
|
25
|
+
def test_header_info
|
26
|
+
assert_equal 59, @dbf.field_count
|
27
|
+
assert_equal 975, @dbf.record_count
|
28
|
+
assert @dbf.has_memo_file?
|
29
|
+
end
|
30
|
+
|
31
|
+
def test_character_fields
|
32
|
+
@dbf.records.each do |record|
|
33
|
+
assert record['NOM'].is_a?(String)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def test_date_fields
|
38
|
+
@dbf.records.each do |record|
|
39
|
+
assert record['DATN'].is_a?(Date) || record['DATN'].nil?
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def test_numeric_fields
|
44
|
+
@dbf.records.each do |record|
|
45
|
+
assert record['NF'].is_a?(Fixnum)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def test_logical_fields
|
50
|
+
# need a test database that has a logical field
|
51
|
+
end
|
52
|
+
|
53
|
+
def test_memo_fields
|
54
|
+
@dbf.records.each_with_index do |record, index|
|
55
|
+
if [1,3,5].include?(index)
|
56
|
+
assert record['OBSE'].is_a?(String)
|
57
|
+
elsif [2].include?(index)
|
58
|
+
assert record['OBSE'].nil?
|
59
|
+
else
|
60
|
+
assert record['OBSE'].is_a?(String) || record['OBSE'].nil?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
metadata
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: dbf
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.1.0
|
7
|
+
date: 2006-08-01 00:00:00 -07:00
|
8
|
+
summary: A library for reading DBase (or XBase, Clipper, Foxpro, etc) database files
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: keithm@infused.org
|
12
|
+
homepage:
|
13
|
+
rubyforge_project:
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: false
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Keith Morrison
|
31
|
+
files:
|
32
|
+
- lib/dbf
|
33
|
+
- lib/dbf.rb
|
34
|
+
- lib/dbf/reader.rb
|
35
|
+
- test/databases
|
36
|
+
- test/read_test.rb
|
37
|
+
- test/databases/foxpro.dbf
|
38
|
+
- test/databases/foxpro.fpt
|
39
|
+
test_files: []
|
40
|
+
|
41
|
+
rdoc_options: []
|
42
|
+
|
43
|
+
extra_rdoc_files: []
|
44
|
+
|
45
|
+
executables: []
|
46
|
+
|
47
|
+
extensions: []
|
48
|
+
|
49
|
+
requirements: []
|
50
|
+
|
51
|
+
dependencies: []
|
52
|
+
|