dbf 0.1.0 → 0.2.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 +66 -29
- data/test/{read_test.rb → foxpro_read_test.rb} +33 -4
- metadata +4 -4
data/lib/dbf/reader.rb
CHANGED
@@ -1,44 +1,42 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'breakpoint'
|
1
3
|
module DBF
|
2
4
|
|
3
5
|
DBF_HEADER_SIZE = 32
|
4
6
|
FPT_HEADER_SIZE = 512
|
5
7
|
FPT_BLOCK_HEADER_SIZE = 8
|
8
|
+
DATE_REGEXP = /([\d]{4})([\d]{2})([\d]{2})/
|
9
|
+
VERSION_DESCRIPTIONS = {"02" => "FoxBase", "03" => "dBase III without memo file", "04" => "dBase IV without memo file",
|
10
|
+
"05" => "dBase V without memo file", "30" => "Visual FoxPro", "31" => "Visual FoxPro with AutoIncrement field",
|
11
|
+
"7b" => "dBase IV with memo file", "83" => "dBase III with memo file", "8b" => "dBase IV with memo file",
|
12
|
+
"8e" => "dBase IV with SQL table", "f5" => "FoxPro with memo file", "fb" => "FoxPro without memo file"}
|
6
13
|
|
7
14
|
class Reader
|
8
15
|
|
9
16
|
attr_reader :field_count
|
10
17
|
attr_reader :fields
|
11
18
|
attr_reader :record_count
|
19
|
+
attr_reader :version
|
20
|
+
attr_reader :last_updated
|
12
21
|
|
13
22
|
def initialize(file)
|
14
23
|
@data_file = File.open(file, 'rb')
|
15
24
|
@memo_file = File.open(file.gsub(/dbf$/i, 'fpt'), 'rb') rescue File.open(file.gsub(/dbf$/i, 'FPT'), 'rb') rescue nil
|
25
|
+
reload!
|
26
|
+
end
|
27
|
+
|
28
|
+
def reload!
|
16
29
|
get_header_info
|
17
30
|
get_memo_header_info if @memo_file
|
18
31
|
get_field_descriptors
|
19
32
|
end
|
20
33
|
|
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
34
|
def has_memo_file?
|
33
35
|
@memo_file ? true : false
|
34
36
|
end
|
35
37
|
|
36
|
-
def get_field_descriptors
|
37
|
-
@fields = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('a10xax4ss'))}
|
38
|
-
end
|
39
|
-
|
40
38
|
def field(field_name)
|
41
|
-
@fields.
|
39
|
+
@fields.detect {|f| f.name == field_name.to_s}
|
42
40
|
end
|
43
41
|
|
44
42
|
def memo(start_block)
|
@@ -51,20 +49,28 @@ module DBF
|
|
51
49
|
memo_string
|
52
50
|
end
|
53
51
|
|
54
|
-
def unpack_string(field)
|
55
|
-
@data_file.read(field.length).unpack("a#{field.length}")
|
56
|
-
end
|
57
|
-
|
58
52
|
def records
|
59
|
-
|
60
|
-
Array.new(@record_count) {build_record if
|
53
|
+
seek_to_record(0)
|
54
|
+
@records ||= Array.new(@record_count) { build_record if active_record? }
|
61
55
|
end
|
62
56
|
|
63
57
|
alias_method :rows, :records
|
64
58
|
|
65
59
|
def record(index)
|
66
60
|
seek_to_record(index)
|
67
|
-
build_record if
|
61
|
+
build_record if active_record?
|
62
|
+
end
|
63
|
+
|
64
|
+
alias_method :row, :record
|
65
|
+
|
66
|
+
def version_description
|
67
|
+
VERSION_DESCRIPTIONS[version]
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def active_record?
|
73
|
+
@data_file.read(1).unpack('H2').to_s == '20' rescue false
|
68
74
|
end
|
69
75
|
|
70
76
|
def build_record
|
@@ -72,22 +78,41 @@ module DBF
|
|
72
78
|
@fields.each do |field|
|
73
79
|
case field.type
|
74
80
|
when 'N' # number
|
75
|
-
|
81
|
+
if field.decimal == 0
|
82
|
+
record[field.name] = unpack_integer(field) rescue nil
|
83
|
+
else
|
84
|
+
record[field.name] = unpack_float(field) rescue nil
|
85
|
+
end
|
76
86
|
when 'D' # date
|
77
87
|
raw = unpack_string(field).to_s.strip
|
78
|
-
record[field.name] = raw.
|
88
|
+
record[field.name] = raw.empty ? nil : Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}) rescue nil
|
79
89
|
when 'M' # memo
|
80
|
-
starting_block =
|
81
|
-
record[field.name] = starting_block == 0 ? nil : memo(starting_block)
|
90
|
+
starting_block = unpack_integer(field)
|
91
|
+
record[field.name] = starting_block == 0 ? nil : memo(starting_block) rescue nil
|
82
92
|
when 'L' # logical
|
83
|
-
record[field.name] = unpack_string(field) =~ /(y|t)/i ? true : false
|
93
|
+
record[field.name] = unpack_string(field) =~ /(y|t)/i ? true : false rescue false
|
84
94
|
else
|
85
|
-
record[field.name] = unpack_string(field)
|
95
|
+
record[field.name] = unpack_string(field)
|
86
96
|
end
|
87
97
|
end
|
88
98
|
record
|
89
99
|
end
|
90
100
|
|
101
|
+
def get_header_info
|
102
|
+
@data_file.rewind
|
103
|
+
@version, @record_count, @header_length, @record_length = @data_file.read(DBF_HEADER_SIZE).unpack('H2xxxVvv')
|
104
|
+
@field_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
|
105
|
+
end
|
106
|
+
|
107
|
+
def get_field_descriptors
|
108
|
+
@fields = Array.new(@field_count) {|i| Field.new(*@data_file.read(32).unpack('a10xax4cc'))}
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_memo_header_info
|
112
|
+
@memo_file.rewind
|
113
|
+
@memo_next_available_block, @memo_block_size = @memo_file.read(FPT_HEADER_SIZE).unpack('Nxxn')
|
114
|
+
end
|
115
|
+
|
91
116
|
def seek(offset)
|
92
117
|
@data_file.seek(@header_length + offset)
|
93
118
|
end
|
@@ -96,6 +121,18 @@ module DBF
|
|
96
121
|
seek(@record_length * index)
|
97
122
|
end
|
98
123
|
|
124
|
+
def unpack_string(field)
|
125
|
+
@data_file.read(field.length).unpack("a#{field.length}").to_s
|
126
|
+
end
|
127
|
+
|
128
|
+
def unpack_integer(field)
|
129
|
+
unpack_string(field).to_i
|
130
|
+
end
|
131
|
+
|
132
|
+
def unpack_float(field)
|
133
|
+
unpack_string(field).to_f
|
134
|
+
end
|
135
|
+
|
99
136
|
end
|
100
137
|
|
101
138
|
class Field
|
@@ -2,7 +2,7 @@ $:.unshift(File.dirname(__FILE__) + "/../lib/")
|
|
2
2
|
require 'test/unit'
|
3
3
|
require 'dbf'
|
4
4
|
|
5
|
-
class
|
5
|
+
class FoxproReadTest < Test::Unit::TestCase
|
6
6
|
|
7
7
|
def setup
|
8
8
|
@dbf = DBF::Reader.new(File.join(File.dirname(__FILE__),'databases', 'foxpro.dbf'))
|
@@ -11,6 +11,9 @@ class ReadTest < Test::Unit::TestCase
|
|
11
11
|
def test_records
|
12
12
|
assert_kind_of Array, @dbf.records
|
13
13
|
assert_kind_of Array, @dbf.rows
|
14
|
+
@dbf.records.each do |record|
|
15
|
+
assert_kind_of DBF::Record, record
|
16
|
+
end
|
14
17
|
end
|
15
18
|
|
16
19
|
def test_record
|
@@ -18,14 +21,23 @@ class ReadTest < Test::Unit::TestCase
|
|
18
21
|
assert_equal @dbf.record(99), @dbf.records[99]
|
19
22
|
end
|
20
23
|
|
24
|
+
def test_fields
|
25
|
+
assert_kind_of Array, @dbf.fields
|
26
|
+
end
|
27
|
+
|
21
28
|
def test_field_count
|
29
|
+
assert_equal 59, @dbf.field_count
|
22
30
|
assert_equal @dbf.field_count, @dbf.fields.size
|
23
31
|
end
|
24
32
|
|
25
|
-
def
|
26
|
-
assert_equal 59, @dbf.field_count
|
33
|
+
def test_record_count
|
27
34
|
assert_equal 975, @dbf.record_count
|
28
|
-
|
35
|
+
assert_equal @dbf.record_count, @dbf.records.size
|
36
|
+
end
|
37
|
+
|
38
|
+
def test_record_length
|
39
|
+
assert_equal 969, @dbf.instance_eval {@record_length}
|
40
|
+
assert_equal 969, @dbf.fields.inject(1) {|sum, field| sum + field.length}
|
29
41
|
end
|
30
42
|
|
31
43
|
def test_character_fields
|
@@ -62,4 +74,21 @@ class ReadTest < Test::Unit::TestCase
|
|
62
74
|
end
|
63
75
|
end
|
64
76
|
|
77
|
+
def test_field
|
78
|
+
assert_kind_of DBF::Field, @dbf.field("NOM")
|
79
|
+
assert_equal "NOM", @dbf.field("NOM").name
|
80
|
+
assert_equal "C", @dbf.field("NOM").type
|
81
|
+
assert_equal 20, @dbf.field(:NOM).length
|
82
|
+
assert_equal 0, @dbf.field("NOM").decimal
|
83
|
+
end
|
84
|
+
|
85
|
+
def test_version
|
86
|
+
assert_equal "f5", @dbf.version
|
87
|
+
assert_equal "FoxPro with memo file", @dbf.version_description
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_has_memo_file
|
91
|
+
assert @dbf.has_memo_file?
|
92
|
+
end
|
93
|
+
|
65
94
|
end
|
metadata
CHANGED
@@ -3,13 +3,13 @@ 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-08-
|
6
|
+
version: 0.2.0
|
7
|
+
date: 2006-08-08 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
|
11
11
|
email: keithm@infused.org
|
12
|
-
homepage:
|
12
|
+
homepage: http://www.infused.org
|
13
13
|
rubyforge_project:
|
14
14
|
description:
|
15
15
|
autorequire:
|
@@ -33,7 +33,7 @@ files:
|
|
33
33
|
- lib/dbf.rb
|
34
34
|
- lib/dbf/reader.rb
|
35
35
|
- test/databases
|
36
|
-
- test/
|
36
|
+
- test/foxpro_read_test.rb
|
37
37
|
- test/databases/foxpro.dbf
|
38
38
|
- test/databases/foxpro.fpt
|
39
39
|
test_files: []
|