dbf 0.4.5 → 0.4.6

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/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  require 'hoe'
2
2
 
3
3
  PKG_NAME = "dbf"
4
- PKG_VERSION = "0.4.5"
4
+ PKG_VERSION = "0.4.6"
5
5
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
6
6
 
7
7
  Hoe.new PKG_NAME, PKG_VERSION do |p|
data/lib/dbf.rb CHANGED
@@ -1,2 +1,4 @@
1
1
  require 'date'
2
+
3
+ require 'dbf/common'
2
4
  require 'dbf/reader'
@@ -1,27 +1,4 @@
1
1
  module DBF
2
-
3
- DBF_HEADER_SIZE = 32
4
- FPT_HEADER_SIZE = 512
5
- FPT_BLOCK_HEADER_SIZE = 8
6
- DATE_REGEXP = /([\d]{4})([\d]{2})([\d]{2})/
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
- }
21
-
22
- class DBFError < StandardError; end
23
- class UnpackError < DBFError; end
24
-
25
2
  class Reader
26
3
  attr_reader :field_count
27
4
  attr_reader :fields
@@ -29,9 +6,9 @@ module DBF
29
6
  attr_reader :version
30
7
  attr_reader :last_updated
31
8
  attr_reader :memo_file_format
9
+ attr_reader :memo_block_size
32
10
 
33
11
  def initialize(file)
34
-
35
12
  @data_file = File.open(file, 'rb')
36
13
  @memo_file = open_memo(file)
37
14
  reload!
@@ -49,7 +26,7 @@ module DBF
49
26
 
50
27
  def open_memo(file)
51
28
  %w(fpt FPT dbt DBT).each do |extension|
52
- filename = file.sub(/dbf$/i, extension)
29
+ filename = file.sub(/#{File.extname(file)[1..-1]}$/, extension)
53
30
  if File.exists?(filename)
54
31
  @memo_file_format = extension.downcase.to_sym
55
32
  return File.open(filename, 'rb')
@@ -62,35 +39,12 @@ module DBF
62
39
  @fields.detect {|f| f.name == field_name.to_s}
63
40
  end
64
41
 
65
- def memo(start_block)
66
- @memo_file.rewind
67
- @memo_file.seek(start_block * @memo_block_size)
68
- if @memo_file_format == :fpt
69
- memo_type, memo_size, memo_string = @memo_file.read(@memo_block_size).unpack("NNa56")
70
- if memo_size > @memo_block_size - FPT_BLOCK_HEADER_SIZE
71
- memo_string << @memo_file.read(memo_size - @memo_block_size + FPT_BLOCK_HEADER_SIZE)
72
- end
73
- else
74
- if version == "83" # dbase iii
75
- memo_string = ""
76
- loop do
77
- memo_string << block = @memo_file.read(512)
78
- break if block.strip.size < 512
79
- end
80
- elsif version == "8b" # dbase iv
81
- memo_type, memo_size = @memo_file.read(8).unpack("LL")
82
- memo_string = @memo_file.read(memo_size)
83
- end
84
- end
85
- memo_string
86
- end
87
-
88
42
  # An array of all the records contained in the database file
89
43
  def records
90
44
  seek_to_record(0)
91
45
  @records ||= Array.new(@record_count) do |i|
92
46
  if active_record?
93
- build_record
47
+ Record.new(self, @data_file, @memo_file)
94
48
  else
95
49
  seek_to_record(i + 1)
96
50
  nil
@@ -100,10 +54,12 @@ module DBF
100
54
 
101
55
  alias_method :rows, :records
102
56
 
103
- # Jump to record
57
+ # Returns the record at <a>index</i> by seeking to the record in the
58
+ # physical database file. See the documentation for the records method for
59
+ # information on how these two methods differ.
104
60
  def record(index)
105
61
  seek_to_record(index)
106
- active_record? ? build_record : nil
62
+ active_record? ? Record.new(self, @data_file, @memo_file) : nil
107
63
  end
108
64
 
109
65
  alias_method :row, :record
@@ -114,39 +70,14 @@ module DBF
114
70
 
115
71
  private
116
72
 
73
+ # Returns false if the record has been marked as deleted, otherwise it returns true. When dBase records are deleted a
74
+ # flag is set, marking the record as deleted. The record will not be fully removed until the database has been compacted.
117
75
  def active_record?
118
76
  @data_file.read(1).unpack('H2').to_s == '20'
119
77
  rescue
120
78
  false
121
79
  end
122
80
 
123
- def build_record
124
- record = Record.new
125
- @fields.each do |field|
126
- case field.type
127
- when 'N' # number
128
- record[field.name] = field.decimal == 0 ? unpack_integer(field) : unpack_float(field) rescue nil
129
- when 'D' # date
130
- raw = unpack_string(field).to_s.strip
131
- unless raw.empty?
132
- begin
133
- record[field.name] = Time.gm(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
134
- rescue
135
- record[field.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i}) rescue nil
136
- end
137
- end
138
- when 'M' # memo
139
- starting_block = unpack_integer(field)
140
- record[field.name] = starting_block == 0 ? nil : memo(starting_block) rescue nil
141
- when 'L' # logical
142
- record[field.name] = unpack_string(field) =~ /^(y|t)$/i ? true : false rescue false
143
- else
144
- record[field.name] = unpack_string(field)
145
- end
146
- end
147
- record
148
- end
149
-
150
81
  def get_header_info
151
82
  @data_file.rewind
152
83
  @version, @record_count, @header_length, @record_length = @data_file.read(DBF_HEADER_SIZE).unpack('H2xxxVvv')
@@ -158,7 +89,7 @@ module DBF
158
89
  @field_count.times do
159
90
  name, type, length, decimal = @data_file.read(32).unpack('a10xax4CC')
160
91
  if length > 0 && !name.strip.empty?
161
- @fields << Field.new(name.strip, type, length, decimal)
92
+ @fields << Field.new(name, type, length, decimal)
162
93
  end
163
94
  end
164
95
  # adjust field count
@@ -181,23 +112,7 @@ module DBF
181
112
  end
182
113
 
183
114
  def seek_to_record(index)
184
- seek(@record_length * index)
185
- end
186
-
187
- def unpack_field(field)
188
- @data_file.read(field.length).unpack("a#{field.length}")
189
- end
190
-
191
- def unpack_string(field)
192
- unpack_field(field).to_s
193
- end
194
-
195
- def unpack_integer(field)
196
- unpack_string(field).to_i
197
- end
198
-
199
- def unpack_float(field)
200
- unpack_string(field).to_f
115
+ seek(index * @record_length)
201
116
  end
202
117
 
203
118
  end
@@ -205,23 +120,83 @@ module DBF
205
120
  class FieldError < StandardError; end
206
121
 
207
122
  class Field
208
- attr_accessor :type, :length, :decimal
123
+ attr_accessor :name, :type, :length, :decimal
209
124
 
210
125
  def initialize(name, type, length, decimal)
211
126
  raise FieldError, "field length must be greater than 0" unless length > 0
212
- self.name, self.type, self.length, self.decimal = name, type, length, decimal
127
+ self.name, self.type, self.length, self.decimal = name.strip, type, length, decimal
213
128
  end
214
129
 
215
130
  def name=(name)
216
131
  @name = name.gsub(/\0/, '')
217
132
  end
218
-
219
- def name
220
- @name
221
- end
133
+
222
134
  end
223
135
 
224
136
  class Record < Hash
137
+
138
+ def initialize(reader, data_file, memo_file)
139
+ @reader, @data_file, @memo_file = reader, data_file, memo_file
140
+ reader.fields.each do |field|
141
+ case field.type
142
+ when 'N' # number
143
+ self[field.name] = field.decimal == 0 ? unpack_string(field).to_i : unpack_string(field).to_f
144
+ when 'D' # date
145
+ raw = unpack_string(field).strip
146
+ unless raw.empty?
147
+ begin
148
+ self[field.name] = Time.gm(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
149
+ rescue
150
+ self[field.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
151
+ end
152
+ end
153
+ when 'M' # memo
154
+ starting_block = unpack_string(field).to_i
155
+ self[field.name] = read_memo(starting_block)
156
+ when 'L' # logical
157
+ self[field.name] = unpack_string(field) =~ /^(y|t)$/i ? true : false
158
+ else
159
+ self[field.name] = unpack_string(field)
160
+ end
161
+ end
162
+ self
163
+ end
164
+
165
+ def unpack_field(field)
166
+ @data_file.read(field.length).unpack("a#{field.length}")
167
+ end
168
+
169
+ def unpack_string(field)
170
+ unpack_field(field).to_s
171
+ end
172
+
173
+ def read_memo(start_block)
174
+ return nil if start_block == 0
175
+ @memo_file.seek(start_block * @reader.memo_block_size)
176
+ if @reader.memo_file_format == :fpt
177
+ memo_type, memo_size, memo_string = @memo_file.read(@reader.memo_block_size).unpack("NNa56")
178
+
179
+ memo_block_content_size = @reader.memo_block_size - FPT_BLOCK_HEADER_SIZE
180
+ if memo_size > memo_block_content_size
181
+ memo_string << @memo_file.read(memo_size - @reader.memo_block_size + FPT_BLOCK_HEADER_SIZE)
182
+ elsif memo_size > 0 and memo_size < memo_block_content_size
183
+ memo_string = memo_string[0, memo_size]
184
+ end
185
+ else
186
+ case @reader.version
187
+ when "83" # dbase iii
188
+ memo_string = ""
189
+ loop do
190
+ memo_string << block = @memo_file.read(512)
191
+ break if block.strip.size < 512
192
+ end
193
+ when "8b" # dbase iv
194
+ memo_type, memo_size = @memo_file.read(8).unpack("LL")
195
+ memo_string = @memo_file.read(memo_size)
196
+ end
197
+ end
198
+ memo_string
199
+ end
225
200
  end
226
201
 
227
202
  end
@@ -85,6 +85,7 @@ module CommonTests
85
85
 
86
86
  def test_memo_fields
87
87
  @controls[:testable_memo_field_names].each do |name|
88
+ assert(@dbf.records.any? {|record| record[name].is_a?(String)}, "expected a String")
88
89
  assert(@dbf.records.any? {|record| record[name].is_a?(String) && record[name].size > 1})
89
90
  end
90
91
  end
@@ -24,4 +24,10 @@ class FoxproReadTest < Test::Unit::TestCase
24
24
  @dbf = DBF::Reader.new(File.join(File.dirname(__FILE__),'databases', 'foxpro.dbf'))
25
25
  end
26
26
 
27
+ # make sure we're grabbing the correct memo
28
+ def test_memo_contents
29
+ assert_equal "jos\202 vicente salvador\r\ncapell\205: salvador vidal\r\nen n\202ixer, les castellers li van fer un pilar i el van entregar al seu pare.",
30
+ @dbf.records[3]['OBSE']
31
+ end
32
+
27
33
  end
metadata CHANGED
@@ -1,10 +1,10 @@
1
1
  --- !ruby/object:Gem::Specification
2
- rubygems_version: 0.9.2
2
+ rubygems_version: 0.9.3
3
3
  specification_version: 1
4
4
  name: dbf
5
5
  version: !ruby/object:Gem::Version
6
- version: 0.4.5
7
- date: 2007-02-12 00:00:00 -08:00
6
+ version: 0.4.6
7
+ date: 2007-05-21 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
@@ -40,10 +40,13 @@ files:
40
40
  - test/databases/dbase_iii.dbf
41
41
  - test/databases/foxpro.dbf
42
42
  - test/databases/foxpro.fpt
43
+ - test/databases/visual_foxpro.dbf
44
+ - test/databases/visual_foxpro.fpt
43
45
  test_files: []
44
46
 
45
- rdoc_options: []
46
-
47
+ rdoc_options:
48
+ - --main
49
+ - README.txt
47
50
  extra_rdoc_files: []
48
51
 
49
52
  executables: []
@@ -60,5 +63,5 @@ dependencies:
60
63
  requirements:
61
64
  - - ">="
62
65
  - !ruby/object:Gem::Version
63
- version: 1.1.7
66
+ version: 1.2.1
64
67
  version: