dbf 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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.select {|f| f.name == field_name}
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
- seek(0)
60
- Array.new(@record_count) {build_record if @data_file.read(1).unpack('c').to_s == '32'}
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 @data_file.read(1).unpack('c').to_s == '32'
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
- record[field.name] = unpack_string(field)[0].to_i
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.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
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 = unpack_string(field).first.to_i
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).to_s.strip
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 ReadTest < Test::Unit::TestCase
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 test_header_info
26
- assert_equal 59, @dbf.field_count
33
+ def test_record_count
27
34
  assert_equal 975, @dbf.record_count
28
- assert @dbf.has_memo_file?
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.1.0
7
- date: 2006-08-01 00:00:00 -07:00
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/read_test.rb
36
+ - test/foxpro_read_test.rb
37
37
  - test/databases/foxpro.dbf
38
38
  - test/databases/foxpro.fpt
39
39
  test_files: []