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 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: []