dbf 1.3.0 → 1.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,88 @@
1
+ module DBF
2
+ class Memo
3
+ BLOCK_HEADER_SIZE = 8
4
+ FPT_HEADER_SIZE = 512
5
+
6
+ def initialize(data, version)
7
+ @data, @version = data, version
8
+ end
9
+
10
+ def get(start_block)
11
+ if start_block > 0
12
+ if format_fpt?
13
+ build_fpt_memo start_block
14
+ else
15
+ build_dbt_memo start_block
16
+ end
17
+ end
18
+ end
19
+
20
+ def close
21
+ @data.close
22
+ end
23
+
24
+ private
25
+
26
+ def format_fpt? #nodoc
27
+ File.extname(@data.path) =~ /fpt/i
28
+ end
29
+
30
+ def build_fpt_memo(start_block) #nodoc
31
+ @data.seek offset(start_block)
32
+
33
+ memo_type, memo_size, memo_string = @data.read(block_size).unpack("NNa*")
34
+ return nil unless memo_type == 1 && memo_size > 0
35
+
36
+ if memo_size > block_content_size
37
+ memo_string << @data.read(content_size(memo_size))
38
+ else
39
+ memo_string = memo_string[0, memo_size]
40
+ end
41
+ memo_string
42
+ end
43
+
44
+ def build_dbt_memo(start_block) #nodoc
45
+ case @version
46
+ when "83" # dbase iii
47
+ build_dbt_83_memo(start_block)
48
+ when "8b" # dbase iv
49
+ build_dbt_8b_memo(start_block)
50
+ end
51
+ end
52
+
53
+ def build_dbt_83_memo(start_block)
54
+ @data.seek offset(start_block)
55
+ memo_string = ""
56
+ begin
57
+ block = @data.read(block_size).gsub(/(\000|\032)/, '')
58
+ memo_string << block
59
+ end until block.size < block_size
60
+ memo_string
61
+ end
62
+
63
+ def build_dbt_8b_memo(start_block)
64
+ @data.seek offset(start_block)
65
+ @data.read(@data.read(BLOCK_HEADER_SIZE).unpack("x4L").first)
66
+ end
67
+
68
+ def offset(start_block) #nodoc
69
+ start_block * block_size
70
+ end
71
+
72
+ def content_size(memo_size) #nodoc
73
+ (memo_size - block_size) + BLOCK_HEADER_SIZE
74
+ end
75
+
76
+ def block_content_size #nodoc
77
+ @block_content_size ||= block_size - BLOCK_HEADER_SIZE
78
+ end
79
+
80
+ def block_size #nodoc
81
+ @block_size ||= begin
82
+ @data.rewind
83
+ format_fpt? ? @data.read(FPT_HEADER_SIZE).unpack('x6n').first || 0 : 512
84
+ end
85
+ end
86
+
87
+ end
88
+ end
@@ -1,21 +1,11 @@
1
1
  module DBF
2
2
  # An instance of DBF::Record represents a row in the DBF file
3
3
  class Record
4
- BLOCK_HEADER_SIZE = 8
5
-
6
- attr_reader :table
7
- attr_reader :attributes
8
- attr_reader :memo_block_size
9
-
10
- delegate :columns, :to => :table
11
-
12
4
  # Initialize a new DBF::Record
13
5
  #
14
6
  # @param [DBF::Table] table
15
- def initialize(table)
16
- @table, @data, @memo = table, table.data, table.memo
17
- @memo_block_size = @table.memo_block_size
18
- initialize_values
7
+ def initialize(data, columns, version, memo)
8
+ @data, @columns, @version, @memo = StringIO.new(data), columns, version, memo
19
9
  define_accessors
20
10
  end
21
11
 
@@ -31,7 +21,7 @@ module DBF
31
21
  #
32
22
  # @return [Array]
33
23
  def to_a
34
- columns.map { |column| @attributes[column.name.underscore] }
24
+ @columns.map { |column| attributes[column.name.underscore] }
35
25
  end
36
26
 
37
27
  # Do all search parameters match?
@@ -42,109 +32,44 @@ module DBF
42
32
  options.all? {|key, value| attributes[key.to_s.underscore] == value}
43
33
  end
44
34
 
35
+ def attributes
36
+ return @attributes if @attributes
37
+
38
+ @attributes = Attributes.new
39
+ @columns.each do |column|
40
+ @attributes[column.name] = init_attribute(column)
41
+ end
42
+ @attributes
43
+ end
44
+
45
45
  private
46
46
 
47
- # Defined attribute accessor methods
48
- def define_accessors
49
- columns.each do |column|
50
- unless self.class.method_defined?(column.name.underscore)
51
- self.class.send :define_method, column.name.underscore do
52
- @attributes[column.name.underscore]
53
- end
47
+ def define_accessors #nodoc
48
+ @columns.each do |column|
49
+ unless self.class.method_defined? column.underscored_name
50
+ self.class.class_eval <<-END
51
+ def #{column.underscored_name}
52
+ @#{column.underscored_name} ||= attributes['#{column.underscored_name}']
53
+ end
54
+ END
54
55
  end
55
56
  end
56
57
  end
57
58
 
58
- # Initialize values for a row
59
- def initialize_values
60
- @attributes = columns.inject(Attributes.new) do |hash, column|
61
- if column.memo?
62
- hash[column.name] = read_memo(get_starting_block(column))
63
- else
64
- hash[column.name] = column.type_cast(unpack_data(column.length))
65
- end
66
- hash
67
- end
59
+ def init_attribute(column) #nodoc
60
+ column.memo? ? @memo.get(get_memo_start_block(column)) : column.type_cast(unpack_data(column))
68
61
  end
69
62
 
70
- # Unpack starting block from database
71
- #
72
- # @param [Fixnum] length
73
- def get_starting_block(column)
74
- if %w(30 31).include?(@table.version)
75
- @data.read(column.length).unpack('V')[0]
63
+ def get_memo_start_block(column) #nodoc
64
+ if %w(30 31).include?(@version)
65
+ @data.read(column.length).unpack('V').first
76
66
  else
77
- unpack_data(column.length).to_i
67
+ unpack_data(column).to_i
78
68
  end
79
69
  end
80
70
 
81
- # Unpack raw data from database
82
- #
83
- # @param [Fixnum] length
84
- def unpack_data(length)
85
- @data.read(length).unpack("a#{length}").first
86
- end
87
-
88
- # Reads a memo from the memo file
89
- #
90
- # @param [Fixnum] start_block
91
- def read_memo(start_block)
92
- return nil if !@table.has_memo_file? || start_block < 1
93
- send "build_#{@table.memo_file_format}_memo", start_block
94
- end
95
-
96
- # Reconstructs a memo from an FPT memo file
97
- #
98
- # @param [Fixnum] start_block
99
- # @return [String]
100
- def build_fpt_memo(start_block)
101
- @memo.seek(start_block * memo_block_size)
102
-
103
- memo_type, memo_size, memo_string = @memo.read(memo_block_size).unpack("NNa*")
104
- return nil unless memo_type == 1 && memo_size > 0
105
-
106
- if memo_size > memo_block_content_size
107
- memo_string << @memo.read(memo_content_size(memo_size))
108
- else
109
- memo_string = memo_string[0, memo_size]
110
- end
111
- memo_string.strip
112
- end
113
-
114
- # Reconstucts a memo from an DBT memo file
115
- #
116
- # @param [Fixnum] start_block
117
- # @return [String]
118
- def build_dbt_memo(start_block)
119
- @memo.seek(start_block * memo_block_size)
120
-
121
- case @table.version
122
- when "83" # dbase iii
123
- memo_string = ""
124
- loop do
125
- block = @memo.read(memo_block_size)
126
- memo_string << block
127
- break if block.tr("\000",'').size < memo_block_size
128
- end
129
- when "8b" # dbase iv
130
- memo_type, memo_size = @memo.read(BLOCK_HEADER_SIZE).unpack("LL")
131
- memo_string = @memo.read(memo_size)
132
- end
133
- memo_string
134
- end
135
-
136
- # The size in bytes of the content for each memo block
137
- #
138
- # @return [Fixnum]
139
- def memo_block_content_size
140
- memo_block_size - BLOCK_HEADER_SIZE
141
- end
142
-
143
- # The size in bytes of the entire memo
144
- #
145
- # @return [Fixnum]
146
- def memo_content_size(memo_size)
147
- (memo_size - memo_block_size) + BLOCK_HEADER_SIZE
71
+ def unpack_data(column) #nodoc
72
+ @data.read(column.length).unpack("a#{column.length}").first
148
73
  end
149
74
 
150
75
  end
@@ -2,9 +2,12 @@ module DBF
2
2
 
3
3
  # DBF::Table is the primary interface to a single DBF file and provides
4
4
  # methods for enumerating and searching the records.
5
+
6
+ # TODO set record_length to length of actual used column lengths
5
7
  class Table
8
+ include Enumerable
9
+
6
10
  DBF_HEADER_SIZE = 32
7
- FPT_HEADER_SIZE = 512
8
11
 
9
12
  VERSION_DESCRIPTIONS = {
10
13
  "02" => "FoxBase",
@@ -21,15 +24,7 @@ module DBF
21
24
  "fb" => "FoxPro without memo file"
22
25
  }
23
26
 
24
- attr_reader :column_count # The total number of columns
25
- attr_reader :columns # An array of DBF::Column
26
27
  attr_reader :version # Internal dBase version number
27
- attr_reader :last_updated # Last updated datetime
28
- attr_reader :memo_file_format # :fpt or :dpt
29
- attr_reader :memo_block_size # The block size for memo records
30
- attr_reader :options # The options hash used to initialize the table
31
- attr_reader :data # DBF file handle
32
- attr_reader :memo # Memo file handle
33
28
  attr_reader :record_count # Total number of records
34
29
 
35
30
  # Opens a DBF::Table
@@ -39,37 +34,14 @@ module DBF
39
34
  # @param [String] path Path to the dbf file
40
35
  def initialize(path)
41
36
  @data = File.open(path, 'rb')
37
+ get_header_info
42
38
  @memo = open_memo(path)
43
- reload!
44
39
  end
45
40
 
46
41
  # Closes the table and memo file
47
42
  def close
43
+ @memo && @memo.close
48
44
  @data.close
49
- @memo.close if @memo
50
- end
51
-
52
- # Reloads the database and memo files
53
- def reload!
54
- @records = nil
55
- get_header_info
56
- get_memo_header_info
57
- get_column_descriptors
58
- end
59
-
60
- # Checks if there is a memo file
61
- #
62
- # @return [Boolean]
63
- def has_memo_file?
64
- @memo ? true : false
65
- end
66
-
67
- # Retrieve a Column by name
68
- #
69
- # @param [String, Symbol] column_name
70
- # @return [DBF::Column]
71
- def column(column_name)
72
- @columns.detect {|f| f.name == column_name.to_s}
73
45
  end
74
46
 
75
47
  # Calls block once for each record in the table. The record may be nil
@@ -77,7 +49,7 @@ module DBF
77
49
  #
78
50
  # @yield [nil, DBF::Record]
79
51
  def each
80
- 0.upto(@record_count - 1) {|index| yield record(index)}
52
+ @record_count.times {|i| yield record(i)}
81
53
  end
82
54
 
83
55
  # Retrieve a record by index number.
@@ -120,40 +92,26 @@ module DBF
120
92
  # t.column :notes, :text
121
93
  # end
122
94
  #
123
- # @param [optional String] path
124
95
  # @return [String]
125
- def schema(path = nil)
96
+ def schema
126
97
  s = "ActiveRecord::Schema.define do\n"
127
98
  s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
128
99
  columns.each do |column|
129
100
  s << " t.column #{column.schema_definition}"
130
101
  end
131
- s << " end\nend"
132
-
133
- if path
134
- File.open(path, 'w') {|f| f.puts(s)}
135
- end
136
-
102
+ s << " end\nend"
137
103
  s
138
104
  end
139
105
 
140
- def to_a
141
- records = []
142
- each {|record| records << record if record}
143
- records
144
- end
145
-
146
106
  # Dumps all records to a CSV file. If no filename is given then CSV is
147
107
  # output to STDOUT.
148
108
  #
149
109
  # @param [optional String] path Defaults to basename of dbf file
150
110
  def to_csv(path = nil)
151
- path = File.basename(@data.path, '.dbf') + '.csv' if path.nil?
152
- FCSV.open(path, 'w', :force_quotes => true) do |csv|
111
+ path = default_csv_path unless path
112
+ csv_class.open(path, 'w', :force_quotes => true) do |csv|
153
113
  csv << columns.map {|c| c.name}
154
- each do |record|
155
- csv << record.to_a
156
- end
114
+ each {|record| csv << record.to_a}
157
115
  end
158
116
  end
159
117
 
@@ -194,121 +152,75 @@ module DBF
194
152
  end
195
153
  end
196
154
 
197
- private
198
-
199
- # Find all matching
200
- #
201
- # @param [Hash] options
202
- # @yield [optional DBF::Record]
203
- # @return [Array]
204
- def find_all(options, &block)
205
- results = []
206
- each do |record|
207
- if record.try(:match?, options)
208
- if block_given?
209
- yield record
210
- else
211
- results << record
212
- end
213
- end
155
+ # Retrieves column information from the database
156
+ def columns
157
+ return @columns if @columns
158
+
159
+ @data.seek(DBF_HEADER_SIZE)
160
+ @columns = []
161
+ column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
162
+ column_count.times do
163
+ name, type, length, decimal = @data.read(32).unpack('a10 x a x4 C2')
164
+ @columns << Column.new(name.strip, type, length, decimal) if length > 0
214
165
  end
215
- results
166
+ @columns
216
167
  end
217
168
 
218
- # Find first matching
219
- #
220
- # @param [Hash] options
221
- # @return [DBF::Record, nil]
222
- def find_first(options)
223
- each do |record|
224
- return record if record.try(:match?, options)
225
- end
226
- nil
227
- end
169
+ private
228
170
 
229
- # Open memo file
230
- #
231
- # @params [String] path
232
- # @return [File]
233
- def open_memo(path)
171
+ def open_memo(path) #nodoc
234
172
  %w(fpt FPT dbt DBT).each do |extname|
235
- filename = replace_extname(path, extname)
173
+ filename = path.sub(/#{File.extname(path)[1..-1]}$/, extname)
236
174
  if File.exists?(filename)
237
- @memo_file_format = extname.downcase.to_sym
238
- return File.open(filename, 'rb')
175
+ return Memo.new(File.open(filename, 'rb'), version)
239
176
  end
240
177
  end
241
178
  nil
242
179
  end
243
180
 
244
- # Replace the file extension
245
- #
246
- # @param [String] path
247
- # @param [String] extension
248
- # @return [String]
249
- def replace_extname(path, extension)
250
- path.sub(/#{File.extname(path)[1..-1]}$/, extension)
181
+ def find_all(options) #nodoc
182
+ map do |record|
183
+ if record.try(:match?, options)
184
+ yield record if block_given?
185
+ record
186
+ end
187
+ end.compact
251
188
  end
252
189
 
253
- # Is record marked for deletion
254
- #
255
- # @return [Boolean]
256
- def deleted_record?
190
+ def find_first(options) #nodoc
191
+ each do |record|
192
+ return record if record.try(:match?, options)
193
+ end
194
+ nil
195
+ end
196
+
197
+ def deleted_record? #nodoc
257
198
  @data.read(1).unpack('a') == ['*']
258
199
  end
259
200
 
260
201
  def current_record
261
- deleted_record? ? nil : DBF::Record.new(self)
202
+ deleted_record? ? nil : DBF::Record.new(@data.read(@record_length), columns, version, @memo)
262
203
  end
263
204
 
264
- # Determine database version, record count, header length and record length
265
- def get_header_info
205
+ def get_header_info #nodoc
266
206
  @data.rewind
267
207
  @version, @record_count, @header_length, @record_length = @data.read(DBF_HEADER_SIZE).unpack('H2 x3 V v2')
268
- @column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
269
208
  end
270
209
 
271
- # Retrieves column information from the database
272
- def get_column_descriptors
273
- @columns = []
274
- @column_count.times do
275
- name, type, length, decimal = @data.read(32).unpack('a10 x a x4 C2')
276
- if length > 0
277
- @columns << Column.new(name.strip, type, length, decimal)
278
- end
279
- end
280
- # Reset the column count in case any were skipped
281
- @column_count = @columns.size
282
-
283
- @columns
210
+ def seek(offset) #nodoc
211
+ @data.seek @header_length + offset
284
212
  end
285
-
286
- # Determines the memo block size and next available block
287
- def get_memo_header_info
288
- if has_memo_file?
289
- @memo.rewind
290
- if @memo_file_format == :fpt
291
- @memo_next_available_block, @memo_block_size = @memo.read(FPT_HEADER_SIZE).unpack('N x2 n')
292
- @memo_block_size = 0 if @memo_block_size.nil?
293
- else
294
- @memo_block_size = 512
295
- @memo_next_available_block = File.size(@memo.path) / @memo_block_size
296
- end
297
- end
213
+
214
+ def seek_to_record(index) #nodoc
215
+ seek index * @record_length
298
216
  end
299
217
 
300
- # Seek to a byte offset
301
- #
302
- # @params [Fixnum] offset
303
- def seek(offset)
304
- @data.seek(@header_length + offset)
218
+ def csv_class #nodoc
219
+ CSV.const_defined?(:Reader) ? FCSV : CSV
305
220
  end
306
-
307
- # Seek to a record
308
- #
309
- # @param [Fixnum] index
310
- def seek_to_record(index)
311
- seek(index * @record_length)
221
+
222
+ def default_csv_path #nodoc
223
+ File.basename(@data.path, '.dbf') + '.csv'
312
224
  end
313
225
 
314
226
  end