dbf 0.4.5 → 0.4.6
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/Rakefile +1 -1
- data/lib/dbf.rb +2 -0
- data/lib/dbf/reader.rb +77 -102
- data/test/common.rb +1 -0
- data/test/databases/visual_foxpro.dbf +0 -0
- data/test/databases/visual_foxpro.fpt +0 -0
- data/test/foxpro_read_test.rb +6 -0
- metadata +9 -6
data/Rakefile
CHANGED
data/lib/dbf.rb
CHANGED
data/lib/dbf/reader.rb
CHANGED
@@ -1,27 +1,4 @@
|
|
1
1
|
module DBF
|
2
|
-
|
3
|
-
DBF_HEADER_SIZE = 32
|
4
|
-
FPT_HEADER_SIZE = 512
|
5
|
-
FPT_BLOCK_HEADER_SIZE = 8
|
6
|
-
DATE_REGEXP = /([\d]{4})([\d]{2})([\d]{2})/
|
7
|
-
VERSION_DESCRIPTIONS = {
|
8
|
-
"02" => "FoxBase",
|
9
|
-
"03" => "dBase III without memo file",
|
10
|
-
"04" => "dBase IV without memo file",
|
11
|
-
"05" => "dBase V without memo file",
|
12
|
-
"30" => "Visual FoxPro",
|
13
|
-
"31" => "Visual FoxPro with AutoIncrement field",
|
14
|
-
"7b" => "dBase IV with memo file",
|
15
|
-
"83" => "dBase III with memo file",
|
16
|
-
"8b" => "dBase IV with memo file",
|
17
|
-
"8e" => "dBase IV with SQL table",
|
18
|
-
"f5" => "FoxPro with memo file",
|
19
|
-
"fb" => "FoxPro without memo file"
|
20
|
-
}
|
21
|
-
|
22
|
-
class DBFError < StandardError; end
|
23
|
-
class UnpackError < DBFError; end
|
24
|
-
|
25
2
|
class Reader
|
26
3
|
attr_reader :field_count
|
27
4
|
attr_reader :fields
|
@@ -29,9 +6,9 @@ module DBF
|
|
29
6
|
attr_reader :version
|
30
7
|
attr_reader :last_updated
|
31
8
|
attr_reader :memo_file_format
|
9
|
+
attr_reader :memo_block_size
|
32
10
|
|
33
11
|
def initialize(file)
|
34
|
-
|
35
12
|
@data_file = File.open(file, 'rb')
|
36
13
|
@memo_file = open_memo(file)
|
37
14
|
reload!
|
@@ -49,7 +26,7 @@ module DBF
|
|
49
26
|
|
50
27
|
def open_memo(file)
|
51
28
|
%w(fpt FPT dbt DBT).each do |extension|
|
52
|
-
filename = file.sub(
|
29
|
+
filename = file.sub(/#{File.extname(file)[1..-1]}$/, extension)
|
53
30
|
if File.exists?(filename)
|
54
31
|
@memo_file_format = extension.downcase.to_sym
|
55
32
|
return File.open(filename, 'rb')
|
@@ -62,35 +39,12 @@ module DBF
|
|
62
39
|
@fields.detect {|f| f.name == field_name.to_s}
|
63
40
|
end
|
64
41
|
|
65
|
-
def memo(start_block)
|
66
|
-
@memo_file.rewind
|
67
|
-
@memo_file.seek(start_block * @memo_block_size)
|
68
|
-
if @memo_file_format == :fpt
|
69
|
-
memo_type, memo_size, memo_string = @memo_file.read(@memo_block_size).unpack("NNa56")
|
70
|
-
if memo_size > @memo_block_size - FPT_BLOCK_HEADER_SIZE
|
71
|
-
memo_string << @memo_file.read(memo_size - @memo_block_size + FPT_BLOCK_HEADER_SIZE)
|
72
|
-
end
|
73
|
-
else
|
74
|
-
if version == "83" # dbase iii
|
75
|
-
memo_string = ""
|
76
|
-
loop do
|
77
|
-
memo_string << block = @memo_file.read(512)
|
78
|
-
break if block.strip.size < 512
|
79
|
-
end
|
80
|
-
elsif version == "8b" # dbase iv
|
81
|
-
memo_type, memo_size = @memo_file.read(8).unpack("LL")
|
82
|
-
memo_string = @memo_file.read(memo_size)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
memo_string
|
86
|
-
end
|
87
|
-
|
88
42
|
# An array of all the records contained in the database file
|
89
43
|
def records
|
90
44
|
seek_to_record(0)
|
91
45
|
@records ||= Array.new(@record_count) do |i|
|
92
46
|
if active_record?
|
93
|
-
|
47
|
+
Record.new(self, @data_file, @memo_file)
|
94
48
|
else
|
95
49
|
seek_to_record(i + 1)
|
96
50
|
nil
|
@@ -100,10 +54,12 @@ module DBF
|
|
100
54
|
|
101
55
|
alias_method :rows, :records
|
102
56
|
|
103
|
-
#
|
57
|
+
# Returns the record at <a>index</i> by seeking to the record in the
|
58
|
+
# physical database file. See the documentation for the records method for
|
59
|
+
# information on how these two methods differ.
|
104
60
|
def record(index)
|
105
61
|
seek_to_record(index)
|
106
|
-
active_record? ?
|
62
|
+
active_record? ? Record.new(self, @data_file, @memo_file) : nil
|
107
63
|
end
|
108
64
|
|
109
65
|
alias_method :row, :record
|
@@ -114,39 +70,14 @@ module DBF
|
|
114
70
|
|
115
71
|
private
|
116
72
|
|
73
|
+
# Returns false if the record has been marked as deleted, otherwise it returns true. When dBase records are deleted a
|
74
|
+
# flag is set, marking the record as deleted. The record will not be fully removed until the database has been compacted.
|
117
75
|
def active_record?
|
118
76
|
@data_file.read(1).unpack('H2').to_s == '20'
|
119
77
|
rescue
|
120
78
|
false
|
121
79
|
end
|
122
80
|
|
123
|
-
def build_record
|
124
|
-
record = Record.new
|
125
|
-
@fields.each do |field|
|
126
|
-
case field.type
|
127
|
-
when 'N' # number
|
128
|
-
record[field.name] = field.decimal == 0 ? unpack_integer(field) : unpack_float(field) rescue nil
|
129
|
-
when 'D' # date
|
130
|
-
raw = unpack_string(field).to_s.strip
|
131
|
-
unless raw.empty?
|
132
|
-
begin
|
133
|
-
record[field.name] = Time.gm(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
|
134
|
-
rescue
|
135
|
-
record[field.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}) rescue nil
|
136
|
-
end
|
137
|
-
end
|
138
|
-
when 'M' # memo
|
139
|
-
starting_block = unpack_integer(field)
|
140
|
-
record[field.name] = starting_block == 0 ? nil : memo(starting_block) rescue nil
|
141
|
-
when 'L' # logical
|
142
|
-
record[field.name] = unpack_string(field) =~ /^(y|t)$/i ? true : false rescue false
|
143
|
-
else
|
144
|
-
record[field.name] = unpack_string(field)
|
145
|
-
end
|
146
|
-
end
|
147
|
-
record
|
148
|
-
end
|
149
|
-
|
150
81
|
def get_header_info
|
151
82
|
@data_file.rewind
|
152
83
|
@version, @record_count, @header_length, @record_length = @data_file.read(DBF_HEADER_SIZE).unpack('H2xxxVvv')
|
@@ -158,7 +89,7 @@ module DBF
|
|
158
89
|
@field_count.times do
|
159
90
|
name, type, length, decimal = @data_file.read(32).unpack('a10xax4CC')
|
160
91
|
if length > 0 && !name.strip.empty?
|
161
|
-
@fields << Field.new(name
|
92
|
+
@fields << Field.new(name, type, length, decimal)
|
162
93
|
end
|
163
94
|
end
|
164
95
|
# adjust field count
|
@@ -181,23 +112,7 @@ module DBF
|
|
181
112
|
end
|
182
113
|
|
183
114
|
def seek_to_record(index)
|
184
|
-
seek(
|
185
|
-
end
|
186
|
-
|
187
|
-
def unpack_field(field)
|
188
|
-
@data_file.read(field.length).unpack("a#{field.length}")
|
189
|
-
end
|
190
|
-
|
191
|
-
def unpack_string(field)
|
192
|
-
unpack_field(field).to_s
|
193
|
-
end
|
194
|
-
|
195
|
-
def unpack_integer(field)
|
196
|
-
unpack_string(field).to_i
|
197
|
-
end
|
198
|
-
|
199
|
-
def unpack_float(field)
|
200
|
-
unpack_string(field).to_f
|
115
|
+
seek(index * @record_length)
|
201
116
|
end
|
202
117
|
|
203
118
|
end
|
@@ -205,23 +120,83 @@ module DBF
|
|
205
120
|
class FieldError < StandardError; end
|
206
121
|
|
207
122
|
class Field
|
208
|
-
attr_accessor :type, :length, :decimal
|
123
|
+
attr_accessor :name, :type, :length, :decimal
|
209
124
|
|
210
125
|
def initialize(name, type, length, decimal)
|
211
126
|
raise FieldError, "field length must be greater than 0" unless length > 0
|
212
|
-
self.name, self.type, self.length, self.decimal = name, type, length, decimal
|
127
|
+
self.name, self.type, self.length, self.decimal = name.strip, type, length, decimal
|
213
128
|
end
|
214
129
|
|
215
130
|
def name=(name)
|
216
131
|
@name = name.gsub(/\0/, '')
|
217
132
|
end
|
218
|
-
|
219
|
-
def name
|
220
|
-
@name
|
221
|
-
end
|
133
|
+
|
222
134
|
end
|
223
135
|
|
224
136
|
class Record < Hash
|
137
|
+
|
138
|
+
def initialize(reader, data_file, memo_file)
|
139
|
+
@reader, @data_file, @memo_file = reader, data_file, memo_file
|
140
|
+
reader.fields.each do |field|
|
141
|
+
case field.type
|
142
|
+
when 'N' # number
|
143
|
+
self[field.name] = field.decimal == 0 ? unpack_string(field).to_i : unpack_string(field).to_f
|
144
|
+
when 'D' # date
|
145
|
+
raw = unpack_string(field).strip
|
146
|
+
unless raw.empty?
|
147
|
+
begin
|
148
|
+
self[field.name] = Time.gm(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
|
149
|
+
rescue
|
150
|
+
self[field.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
|
151
|
+
end
|
152
|
+
end
|
153
|
+
when 'M' # memo
|
154
|
+
starting_block = unpack_string(field).to_i
|
155
|
+
self[field.name] = read_memo(starting_block)
|
156
|
+
when 'L' # logical
|
157
|
+
self[field.name] = unpack_string(field) =~ /^(y|t)$/i ? true : false
|
158
|
+
else
|
159
|
+
self[field.name] = unpack_string(field)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
self
|
163
|
+
end
|
164
|
+
|
165
|
+
def unpack_field(field)
|
166
|
+
@data_file.read(field.length).unpack("a#{field.length}")
|
167
|
+
end
|
168
|
+
|
169
|
+
def unpack_string(field)
|
170
|
+
unpack_field(field).to_s
|
171
|
+
end
|
172
|
+
|
173
|
+
def read_memo(start_block)
|
174
|
+
return nil if start_block == 0
|
175
|
+
@memo_file.seek(start_block * @reader.memo_block_size)
|
176
|
+
if @reader.memo_file_format == :fpt
|
177
|
+
memo_type, memo_size, memo_string = @memo_file.read(@reader.memo_block_size).unpack("NNa56")
|
178
|
+
|
179
|
+
memo_block_content_size = @reader.memo_block_size - FPT_BLOCK_HEADER_SIZE
|
180
|
+
if memo_size > memo_block_content_size
|
181
|
+
memo_string << @memo_file.read(memo_size - @reader.memo_block_size + FPT_BLOCK_HEADER_SIZE)
|
182
|
+
elsif memo_size > 0 and memo_size < memo_block_content_size
|
183
|
+
memo_string = memo_string[0, memo_size]
|
184
|
+
end
|
185
|
+
else
|
186
|
+
case @reader.version
|
187
|
+
when "83" # dbase iii
|
188
|
+
memo_string = ""
|
189
|
+
loop do
|
190
|
+
memo_string << block = @memo_file.read(512)
|
191
|
+
break if block.strip.size < 512
|
192
|
+
end
|
193
|
+
when "8b" # dbase iv
|
194
|
+
memo_type, memo_size = @memo_file.read(8).unpack("LL")
|
195
|
+
memo_string = @memo_file.read(memo_size)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
memo_string
|
199
|
+
end
|
225
200
|
end
|
226
201
|
|
227
202
|
end
|
data/test/common.rb
CHANGED
@@ -85,6 +85,7 @@ module CommonTests
|
|
85
85
|
|
86
86
|
def test_memo_fields
|
87
87
|
@controls[:testable_memo_field_names].each do |name|
|
88
|
+
assert(@dbf.records.any? {|record| record[name].is_a?(String)}, "expected a String")
|
88
89
|
assert(@dbf.records.any? {|record| record[name].is_a?(String) && record[name].size > 1})
|
89
90
|
end
|
90
91
|
end
|
Binary file
|
Binary file
|
data/test/foxpro_read_test.rb
CHANGED
@@ -24,4 +24,10 @@ class FoxproReadTest < Test::Unit::TestCase
|
|
24
24
|
@dbf = DBF::Reader.new(File.join(File.dirname(__FILE__),'databases', 'foxpro.dbf'))
|
25
25
|
end
|
26
26
|
|
27
|
+
# make sure we're grabbing the correct memo
|
28
|
+
def test_memo_contents
|
29
|
+
assert_equal "jos\202 vicente salvador\r\ncapell\205: salvador vidal\r\nen n\202ixer, les castellers li van fer un pilar i el van entregar al seu pare.",
|
30
|
+
@dbf.records[3]['OBSE']
|
31
|
+
end
|
32
|
+
|
27
33
|
end
|
metadata
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
|
-
rubygems_version: 0.9.
|
2
|
+
rubygems_version: 0.9.3
|
3
3
|
specification_version: 1
|
4
4
|
name: dbf
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.4.
|
7
|
-
date: 2007-
|
6
|
+
version: 0.4.6
|
7
|
+
date: 2007-05-21 00:00:00 -07:00
|
8
8
|
summary: A library for reading dBase (or xBase, Clipper, Foxpro, etc) database files
|
9
9
|
require_paths:
|
10
10
|
- lib
|
@@ -40,10 +40,13 @@ files:
|
|
40
40
|
- test/databases/dbase_iii.dbf
|
41
41
|
- test/databases/foxpro.dbf
|
42
42
|
- test/databases/foxpro.fpt
|
43
|
+
- test/databases/visual_foxpro.dbf
|
44
|
+
- test/databases/visual_foxpro.fpt
|
43
45
|
test_files: []
|
44
46
|
|
45
|
-
rdoc_options:
|
46
|
-
|
47
|
+
rdoc_options:
|
48
|
+
- --main
|
49
|
+
- README.txt
|
47
50
|
extra_rdoc_files: []
|
48
51
|
|
49
52
|
executables: []
|
@@ -60,5 +63,5 @@ dependencies:
|
|
60
63
|
requirements:
|
61
64
|
- - ">="
|
62
65
|
- !ruby/object:Gem::Version
|
63
|
-
version: 1.1
|
66
|
+
version: 1.2.1
|
64
67
|
version:
|