dbf 0.3.0 → 0.4.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/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
|