dbf 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/lib/dbf/reader.rb +65 -21
- data/test/common.rb +6 -2
- data/test/dbase_iii_read_test.rb +1 -0
- data/test/foxpro_read_test.rb +1 -0
- data/test/performance.rb +14 -0
- metadata +5 -4
data/lib/dbf/reader.rb
CHANGED
@@ -1,21 +1,28 @@
|
|
1
|
-
require 'rubygems'
|
2
|
-
require 'breakpoint'
|
3
1
|
module DBF
|
4
2
|
|
5
3
|
DBF_HEADER_SIZE = 32
|
6
4
|
FPT_HEADER_SIZE = 512
|
7
5
|
FPT_BLOCK_HEADER_SIZE = 8
|
8
6
|
DATE_REGEXP = /([\d]{4})([\d]{2})([\d]{2})/
|
9
|
-
VERSION_DESCRIPTIONS = {
|
10
|
-
"
|
11
|
-
"
|
12
|
-
"
|
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
|
+
}
|
13
21
|
|
14
22
|
class DBFError < StandardError; end
|
15
23
|
class UnpackError < DBFError; end
|
16
24
|
|
17
25
|
class Reader
|
18
|
-
|
19
26
|
attr_reader :field_count
|
20
27
|
attr_reader :fields
|
21
28
|
attr_reader :record_count
|
@@ -24,7 +31,7 @@ module DBF
|
|
24
31
|
|
25
32
|
def initialize(file)
|
26
33
|
@data_file = File.open(file, 'rb')
|
27
|
-
@memo_file =
|
34
|
+
@memo_file = open_memo(file)
|
28
35
|
reload!
|
29
36
|
end
|
30
37
|
|
@@ -38,6 +45,17 @@ module DBF
|
|
38
45
|
@memo_file ? true : false
|
39
46
|
end
|
40
47
|
|
48
|
+
def open_memo(file)
|
49
|
+
%w(fpt FPT dbt DBT).each do |extension|
|
50
|
+
filename = file.sub(/dbf$/i, extension)
|
51
|
+
if File.exists?(filename)
|
52
|
+
@memo_file_format = extension.downcase.to_sym
|
53
|
+
return File.open(filename)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
nil
|
57
|
+
end
|
58
|
+
|
41
59
|
def field(field_name)
|
42
60
|
@fields.detect {|f| f.name == field_name.to_s}
|
43
61
|
end
|
@@ -45,23 +63,42 @@ module DBF
|
|
45
63
|
def memo(start_block)
|
46
64
|
@memo_file.rewind
|
47
65
|
@memo_file.seek(start_block * @memo_block_size)
|
48
|
-
|
49
|
-
|
50
|
-
|
66
|
+
if @memo_file_format == :fpt
|
67
|
+
memo_type, memo_size, memo_string = @memo_file.read(@memo_block_size).unpack("NNa56")
|
68
|
+
if memo_size > @memo_block_size - FPT_BLOCK_HEADER_SIZE
|
69
|
+
memo_string << @memo_file.read(memo_size - @memo_block_size + FPT_BLOCK_HEADER_SIZE)
|
70
|
+
end
|
71
|
+
else
|
72
|
+
if version == "83" # dbase iii
|
73
|
+
memo_string = ""
|
74
|
+
loop do
|
75
|
+
memo_string << block = @memo_file.read(512)
|
76
|
+
break if block.strip.size < 512
|
77
|
+
end
|
78
|
+
end
|
51
79
|
end
|
52
80
|
memo_string
|
53
81
|
end
|
54
82
|
|
83
|
+
# An array of all the records contained in the database file
|
55
84
|
def records
|
56
85
|
seek_to_record(0)
|
57
|
-
@records ||= Array.new(@record_count)
|
86
|
+
@records ||= Array.new(@record_count) do |i|
|
87
|
+
if active_record?
|
88
|
+
build_record
|
89
|
+
else
|
90
|
+
seek_to_record(i + 1)
|
91
|
+
nil
|
92
|
+
end
|
93
|
+
end
|
58
94
|
end
|
59
95
|
|
60
96
|
alias_method :rows, :records
|
61
97
|
|
98
|
+
# Jump to record
|
62
99
|
def record(index)
|
63
100
|
seek_to_record(index)
|
64
|
-
build_record
|
101
|
+
active_record? ? build_record : nil
|
65
102
|
end
|
66
103
|
|
67
104
|
alias_method :row, :record
|
@@ -81,14 +118,16 @@ module DBF
|
|
81
118
|
@fields.each do |field|
|
82
119
|
case field.type
|
83
120
|
when 'N' # number
|
84
|
-
|
85
|
-
record[field.name] = unpack_integer(field) rescue nil
|
86
|
-
else
|
87
|
-
record[field.name] = unpack_float(field) rescue nil
|
88
|
-
end
|
121
|
+
record[field.name] = field.decimal == 0 ? unpack_integer(field) : unpack_float(field) rescue nil
|
89
122
|
when 'D' # date
|
90
123
|
raw = unpack_string(field).to_s.strip
|
91
|
-
|
124
|
+
unless raw.empty?
|
125
|
+
begin
|
126
|
+
record[field.name] = Time.gm(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
|
127
|
+
rescue
|
128
|
+
record[field.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}) rescue nil
|
129
|
+
end
|
130
|
+
end
|
92
131
|
when 'M' # memo
|
93
132
|
starting_block = unpack_integer(field)
|
94
133
|
record[field.name] = starting_block == 0 ? nil : memo(starting_block) rescue nil
|
@@ -108,12 +147,17 @@ module DBF
|
|
108
147
|
end
|
109
148
|
|
110
149
|
def get_field_descriptors
|
111
|
-
@fields = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('
|
150
|
+
@fields = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('a10xax4CC'))}
|
112
151
|
end
|
113
152
|
|
114
153
|
def get_memo_header_info
|
115
154
|
@memo_file.rewind
|
116
|
-
|
155
|
+
if @memo_file_format == :fpt
|
156
|
+
@memo_next_available_block, @memo_block_size = @memo_file.read(FPT_HEADER_SIZE).unpack('Nxxn')
|
157
|
+
else
|
158
|
+
@memo_block_size = 512
|
159
|
+
@memo_next_available_block = File.size(@memo_file.path) / @memo_block_size
|
160
|
+
end
|
117
161
|
end
|
118
162
|
|
119
163
|
def seek(offset)
|
data/test/common.rb
CHANGED
@@ -10,6 +10,10 @@ module CommonTests
|
|
10
10
|
assert_equal @controls[:has_memo_file], @dbf.has_memo_file?
|
11
11
|
end
|
12
12
|
|
13
|
+
def test_memo_file_format
|
14
|
+
assert_equal @controls[:memo_file_format], @dbf.instance_eval("@memo_file_format")
|
15
|
+
end
|
16
|
+
|
13
17
|
def test_records
|
14
18
|
assert_kind_of Array, @dbf.records
|
15
19
|
assert_kind_of Array, @dbf.rows
|
@@ -59,7 +63,7 @@ module CommonTests
|
|
59
63
|
|
60
64
|
def test_date_fields
|
61
65
|
@controls[:testable_date_field_names].each do |name|
|
62
|
-
assert(@dbf.records.any? {|record| record[name].is_a?(Date)})
|
66
|
+
assert(@dbf.records.any? {|record| record[name].is_a?(Date) || record[name].is_a?(Time)})
|
63
67
|
end
|
64
68
|
end
|
65
69
|
|
@@ -81,7 +85,7 @@ module CommonTests
|
|
81
85
|
|
82
86
|
def test_memo_fields
|
83
87
|
@controls[:testable_memo_field_names].each do |name|
|
84
|
-
assert(@dbf.records.any? {|record| record[name].is_a?(String)})
|
88
|
+
assert(@dbf.records.any? {|record| record[name].is_a?(String) && record[name].size > 1})
|
85
89
|
end
|
86
90
|
end
|
87
91
|
|
data/test/dbase_iii_read_test.rb
CHANGED
data/test/foxpro_read_test.rb
CHANGED
data/test/performance.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
$:.unshift(File.dirname(__FILE__) + "/../lib/")
|
4
|
+
require 'dbf'
|
5
|
+
require 'profiler'
|
6
|
+
|
7
|
+
dbf = DBF::Reader.new(File.join(File.dirname(__FILE__),'databases', 'foxpro.dbf'))
|
8
|
+
|
9
|
+
Profiler__::start_profile
|
10
|
+
|
11
|
+
dbf.records
|
12
|
+
|
13
|
+
Profiler__::stop_profile
|
14
|
+
Profiler__::print_profile($stdout)
|
metadata
CHANGED
@@ -3,9 +3,9 @@ rubygems_version: 0.9.0
|
|
3
3
|
specification_version: 1
|
4
4
|
name: dbf
|
5
5
|
version: !ruby/object:Gem::Version
|
6
|
-
version: 0.
|
7
|
-
date: 2006-
|
8
|
-
summary: A library for reading
|
6
|
+
version: 0.4.0
|
7
|
+
date: 2006-10-07 00:00:00 -07:00
|
8
|
+
summary: A library for reading dBase (or xBase, Clipper, Foxpro, etc) database files
|
9
9
|
require_paths:
|
10
10
|
- lib
|
11
11
|
email: keithm@infused.org
|
@@ -15,7 +15,7 @@ description:
|
|
15
15
|
autorequire:
|
16
16
|
default_executable:
|
17
17
|
bindir: bin
|
18
|
-
has_rdoc:
|
18
|
+
has_rdoc: true
|
19
19
|
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
20
|
requirements:
|
21
21
|
- - ">"
|
@@ -36,6 +36,7 @@ files:
|
|
36
36
|
- test/databases
|
37
37
|
- test/dbase_iii_read_test.rb
|
38
38
|
- test/foxpro_read_test.rb
|
39
|
+
- test/performance.rb
|
39
40
|
- test/databases/dbase_iii.dbf
|
40
41
|
- test/databases/foxpro.dbf
|
41
42
|
- test/databases/foxpro.fpt
|