dbf 0.5.4 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,3 +1,7 @@
1
+ == 0.5.4
2
+
3
+ * Ignore deleted records in both memory modes
4
+
1
5
  == 0.5.3
2
6
 
3
7
  * Added a standalone dbf utility (try dbf -h for help)
@@ -1,4 +1,3 @@
1
- .DS_Store
2
1
  History.txt
3
2
  Manifest.txt
4
3
  README.txt
@@ -7,12 +6,10 @@ benchmarks/performance.rb
7
6
  benchmarks/seek_benchmark.rb
8
7
  bin/dbf
9
8
  lib/dbf.rb
10
- lib/dbf/field.rb
9
+ lib/dbf/column.rb
11
10
  lib/dbf/globals.rb
12
- lib/dbf/reader.rb
13
11
  lib/dbf/record.rb
14
- spec/.DS_Store
15
- spec/fixtures/.DS_Store
12
+ lib/dbf/table.rb
16
13
  spec/fixtures/dbase_03.dbf
17
14
  spec/fixtures/dbase_30.dbf
18
15
  spec/fixtures/dbase_30.fpt
@@ -30,6 +27,6 @@ spec/functional/format_83_spec.rb
30
27
  spec/functional/format_8b_spec.rb
31
28
  spec/functional/format_f5_spec.rb
32
29
  spec/spec_helper.rb
33
- spec/unit/field_spec.rb
34
- spec/unit/reader_spec.rb
30
+ spec/unit/column_spec.rb
35
31
  spec/unit/record_spec.rb
32
+ spec/unit/table_spec.rb
data/README.txt CHANGED
@@ -29,30 +29,33 @@ Copyright (c) 2006-2007 Keith Morrison <keithm@infused.org, www.infused.org>
29
29
  require 'rubygems'
30
30
  require 'dbf'
31
31
 
32
- reader = DBF::Reader.new("old_data.dbf")
32
+ table = DBF::Table.new("old_data.dbf")
33
33
 
34
34
  # Print the 'name' field from record number 4
35
- puts reader.record(4)['name']
35
+ puts table.record(4).name
36
+
37
+ # Attributes can also be accessed using the column name as a Hash key
38
+ puts table.record(4)["name"]
36
39
 
37
40
  # Print the 'name' and 'address' fields from each record
38
- reader.records.each do |record|
39
- puts record['name']
40
- puts record['email']
41
+ table.records.each do |record|
42
+ puts record.name
43
+ puts record.email
41
44
  end
42
45
 
43
46
  # Find records
44
- reader.find :all, :first_name => 'Keith'
45
- reader.find :all, :first_name => 'Keith', :last_name => 'Morrison'
46
- reader.find :first, :first_name => 'Keith'
47
- reader.find(10)
47
+ table.find :all, :first_name => 'Keith'
48
+ table.find :all, :first_name => 'Keith', :last_name => 'Morrison'
49
+ table.find :first, :first_name => 'Keith'
50
+ table.find(10)
48
51
 
49
52
  == Large databases
50
53
 
51
- DBF::Reader defaults to loading all records into memory. This may not be what
54
+ DBF::Table defaults to loading all records into memory. This may not be what
52
55
  you want, especially if the database is large. To disable this behavior, set
53
56
  the in_memory option to false during initialization.
54
57
 
55
- reader = DBF::Reader.new("old_data.dbf", :in_memory => false)
58
+ table = DBF::Table.new("old_data.dbf", :in_memory => false)
56
59
 
57
60
  == Command-line utility
58
61
 
@@ -66,8 +69,8 @@ A small command-line utility called dbf is installed along with the gem.
66
69
 
67
70
  == Limitations and known bugs
68
71
 
69
- * DBF is read-only at the moment
70
- * Index files are not utilized
72
+ * DBF is read-only
73
+ * Index files are not used
71
74
 
72
75
  == License
73
76
 
data/Rakefile CHANGED
@@ -2,7 +2,7 @@ require 'hoe'
2
2
  require 'spec/rake/spectask'
3
3
 
4
4
  PKG_NAME = "dbf"
5
- PKG_VERSION = "0.5.4"
5
+ PKG_VERSION = "1.0.0"
6
6
  PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
7
7
 
8
8
  Hoe.new PKG_NAME, PKG_VERSION do |p|
data/bin/dbf CHANGED
@@ -33,7 +33,7 @@ else
33
33
  puts "\nFields:"
34
34
  puts "Name Type Length Decimal"
35
35
  puts "-" * 78
36
- reader.fields.each do |f|
36
+ reader.columns.each do |f|
37
37
  puts "%-16s %-10s %-10s %-10s" % [f.name, f.type, f.length, f.decimal]
38
38
  end
39
39
  end
data/lib/dbf.rb CHANGED
@@ -2,5 +2,5 @@ require 'date'
2
2
 
3
3
  require 'dbf/globals'
4
4
  require 'dbf/record'
5
- require 'dbf/field'
6
- require 'dbf/reader'
5
+ require 'dbf/column'
6
+ require 'dbf/table'
@@ -1,11 +1,11 @@
1
1
  module DBF
2
- class FieldLengthError < DBFError; end
2
+ class ColumnLengthError < DBFError; end
3
3
 
4
- class Field
4
+ class Column
5
5
  attr_reader :name, :type, :length, :decimal
6
6
 
7
7
  def initialize(name, type, length, decimal)
8
- raise FieldLengthError, "field length must be greater than 0" unless length > 0
8
+ raise ColumnLengthError, "field length must be greater than 0" unless length > 0
9
9
  @name, @type, @length, @decimal = name.gsub(/\0/, ''), type, length, decimal
10
10
  end
11
11
  end
@@ -1,71 +1,102 @@
1
1
  module DBF
2
- class Record < Hash
3
- def initialize(reader, data_file, memo_file)
4
- @reader, @data_file, @memo_file = reader, data_file, memo_file
5
- reader.fields.each do |field|
6
- case field.type
2
+ class Record
3
+ attr_reader :attributes
4
+
5
+ @@accessors_defined = false
6
+
7
+ def initialize(table)
8
+ @table, @data, @memo = table, table.data, table.memo
9
+ @attributes = {}
10
+ initialize_values(table.columns)
11
+ define_accessors
12
+ self
13
+ end
14
+
15
+ private
16
+
17
+ def define_accessors
18
+ return if @@accessors_defined
19
+ @table.columns.each do |column|
20
+ underscored_column_name = underscore(column.name)
21
+ if @table.options[:accessors] && !respond_to?(underscored_column_name)
22
+ self.class.send :define_method, underscored_column_name do
23
+ @attributes[column.name]
24
+ end
25
+ @@accessors_defined = true
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize_values(columns)
31
+ columns.each do |column|
32
+ case column.type
7
33
  when 'N' # number
8
- self[field.name] = field.decimal == 0 ? unpack_string(field).to_i : unpack_string(field).to_f
34
+ @attributes[column.name] = column.decimal.zero? ? unpack_string(column).to_i : unpack_string(column).to_f
9
35
  when 'D' # date
10
- raw = unpack_string(field).strip
36
+ raw = unpack_string(column).strip
11
37
  unless raw.empty?
12
38
  begin
13
- self[field.name] = Time.gm(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
39
+ @attributes[column.name] = Time.gm(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
14
40
  rescue
15
- self[field.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
41
+ @attributes[column.name] = Date.new(*raw.match(DATE_REGEXP).to_a.slice(1,3).map {|n| n.to_i})
16
42
  end
17
43
  end
18
44
  when 'M' # memo
19
- starting_block = unpack_string(field).to_i
20
- self[field.name] = read_memo(starting_block)
45
+ starting_block = unpack_string(column).to_i
46
+ @attributes[column.name] = read_memo(starting_block)
21
47
  when 'L' # logical
22
- self[field.name] = unpack_string(field) =~ /^(y|t)$/i ? true : false
48
+ @attributes[column.name] = unpack_string(column) =~ /^(y|t)$/i ? true : false
23
49
  else
24
- self[field.name] = unpack_string(field).strip
50
+ @attributes[column.name] = unpack_string(column).strip
25
51
  end
26
52
  end
27
- self
28
53
  end
29
-
30
- private
31
-
32
- def unpack_field(field)
33
- @data_file.read(field.length).unpack("a#{field.length}")
34
- end
35
-
36
- def unpack_string(field)
37
- unpack_field(field).to_s
38
- end
39
-
40
- def read_memo(start_block)
41
- return nil if start_block == 0
42
- @memo_file.seek(start_block * @reader.memo_block_size)
43
- if @reader.memo_file_format == :fpt
44
- memo_type, memo_size, memo_string = @memo_file.read(@reader.memo_block_size).unpack("NNa56")
54
+
55
+ def unpack_column(column)
56
+ @data.read(column.length).unpack("a#{column.length}")
57
+ end
58
+
59
+ def unpack_string(column)
60
+ unpack_column(column).to_s
61
+ end
62
+
63
+ def read_memo(start_block)
64
+ return nil if start_block == 0
65
+ @memo.seek(start_block * @table.memo_block_size)
66
+ if @table.memo_file_format == :fpt
67
+ memo_type, memo_size, memo_string = @memo.read(@table.memo_block_size).unpack("NNa56")
68
+
69
+ # skip the memo if it isn't text
70
+ return nil unless memo_type == 1
45
71
 
46
- # skip the memo if it isn't texst
47
- return nil unless memo_type == 1
48
-
49
- memo_block_content_size = @reader.memo_block_size - FPT_BLOCK_HEADER_SIZE
50
- if memo_size > memo_block_content_size
51
- memo_string << @memo_file.read(memo_size - @reader.memo_block_size + FPT_BLOCK_HEADER_SIZE)
52
- elsif memo_size > 0 and memo_size < memo_block_content_size
53
- memo_string = memo_string[0, memo_size]
54
- end
55
- else
56
- case @reader.version
57
- when "83" # dbase iii
58
- memo_string = ""
59
- loop do
60
- memo_string << block = @memo_file.read(512)
61
- break if block.strip.size < 512
62
- end
63
- when "8b" # dbase iv
64
- memo_type, memo_size = @memo_file.read(8).unpack("LL")
65
- memo_string = @memo_file.read(memo_size)
72
+ memo_block_content_size = @table.memo_block_size - FPT_BLOCK_HEADER_SIZE
73
+ if memo_size > memo_block_content_size
74
+ memo_string << @memo.read(memo_size - @table.memo_block_size + FPT_BLOCK_HEADER_SIZE)
75
+ elsif memo_size > 0 and memo_size < memo_block_content_size
76
+ memo_string = memo_string[0, memo_size]
77
+ end
78
+ else
79
+ case @table.version
80
+ when "83" # dbase iii
81
+ memo_string = ""
82
+ loop do
83
+ memo_string << block = @memo.read(512)
84
+ break if block.strip.size < 512
66
85
  end
86
+ when "8b" # dbase iv
87
+ memo_type, memo_size = @memo.read(8).unpack("LL")
88
+ memo_string = @memo.read(memo_size)
67
89
  end
68
- memo_string
69
90
  end
91
+ memo_string
92
+ end
93
+
94
+ def underscore(camel_cased_word)
95
+ camel_cased_word.to_s.gsub(/::/, '/').
96
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
97
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
98
+ tr("-", "_").
99
+ downcase
70
100
  end
101
+ end
71
102
  end
@@ -1,10 +1,11 @@
1
1
  module DBF
2
- class Reader
3
- # The total number of fields (columns)
4
- attr_reader :field_count
2
+
3
+ class Table
4
+ # The total number of columns (columns)
5
+ attr_reader :column_count
5
6
 
6
- # An array of DBF::Field records
7
- attr_reader :fields
7
+ # An array of DBF::Column records
8
+ attr_reader :columns
8
9
 
9
10
  # Internal dBase version number
10
11
  attr_reader :version
@@ -18,15 +19,22 @@ module DBF
18
19
  # The block size for memo records
19
20
  attr_reader :memo_block_size
20
21
 
22
+ # The options that were used when initializing DBF::Table. This is a Hash.
23
+ attr_reader :options
24
+
25
+ attr_reader :data
26
+ attr_reader :memo
27
+
21
28
  # Initialize a new DBF::Reader.
22
29
  # Example:
23
30
  # reader = DBF::Reader.new 'data.dbf'
24
31
  def initialize(filename, options = {})
25
- options = {:in_memory => true}.merge(options)
32
+ @options = {:in_memory => true, :accessors => true}.merge(options)
26
33
 
27
- @in_memory = options[:in_memory]
28
- @data_file = File.open(filename, 'rb')
29
- @memo_file = open_memo(filename)
34
+ @in_memory = @options[:in_memory]
35
+ @accessors = @options[:accessors]
36
+ @data = File.open(filename, 'rb')
37
+ @memo = open_memo(filename)
30
38
  reload!
31
39
  end
32
40
 
@@ -34,21 +42,14 @@ module DBF
34
42
  def reload!
35
43
  @records = nil
36
44
  get_header_info
37
- get_memo_header_info if @memo_file
38
- get_field_descriptors
45
+ get_memo_header_info if @memo
46
+ get_column_descriptors
39
47
  build_db_index
40
48
  end
41
49
 
42
50
  # Returns true if there is a corresponding memo file
43
51
  def has_memo_file?
44
- @memo_file ? true : false
45
- end
46
-
47
- # If true, DBF::Reader will load all records into memory. If false, records are retrieved using file I/O.
48
- # You can set this option is set during initialization of the DBF::Reader. Defaults to true. Example:
49
- # reader = DBF::Reader.new 'data.dbf', :in_memory => false
50
- def in_memory?
51
- @in_memory
52
+ @memo ? true : false
52
53
  end
53
54
 
54
55
  # The total number of active records.
@@ -56,16 +57,16 @@ module DBF
56
57
  @db_index.size
57
58
  end
58
59
 
59
- # Returns an instance of DBF::Field for <b>field_name</b>. <b>field_name</b>
60
+ # Returns an instance of DBF::Column for <b>column_name</b>. <b>column_name</b>
60
61
  # can be a symbol or a string.
61
- def field(field_name)
62
- @fields.detect {|f| f.name == field_name.to_s}
62
+ def column(column_name)
63
+ @columns.detect {|f| f.name == column_name.to_s}
63
64
  end
64
65
 
65
66
  # An array of all the records contained in the database file. Each record is an instance
66
67
  # of DBF::Record (or nil if the record is marked for deletion).
67
68
  def records
68
- if in_memory?
69
+ if options[:in_memory]
69
70
  @records ||= get_all_records_from_file
70
71
  else
71
72
  get_all_records_from_file
@@ -76,14 +77,16 @@ module DBF
76
77
 
77
78
  # Returns a DBF::Record (or nil if the record has been marked for deletion) for the record at <tt>index</tt>.
78
79
  def record(index)
79
- if in_memory?
80
+ if options[:in_memory]
80
81
  records[index]
81
82
  else
82
83
  get_record_from_file(index)
83
84
  end
84
85
  end
85
86
 
86
- # Find records. Examples:
87
+ # Find records using a simple ActiveRecord-like syntax.
88
+ #
89
+ # Examples:
87
90
  # reader = DBF::Reader.new 'mydata.dbf'
88
91
  #
89
92
  # # Find record number 5
@@ -97,25 +100,23 @@ module DBF
97
100
  #
98
101
  # The <b>command</b> can be an id, :all, or :first.
99
102
  # <b>options</b> is optional and, if specified, should be a hash where the keys correspond
100
- # to field names in the database. The values will be matched exactly with the value
103
+ # to column names in the database. The values will be matched exactly with the value
101
104
  # in the database. If you specify more than one key, all values must match in order
102
105
  # for the record to be returned. The equivalent SQL would be "WHERE key1 = 'value1'
103
106
  # AND key2 = 'value2'".
104
107
  def find(command, options = {})
105
108
  case command
106
109
  when Fixnum
107
- if !options.empty?
108
- raise ArgumentError, "options are not allowed when command is a record number"
109
- end
110
110
  record(command)
111
111
  when :all
112
+ return records if options.empty?
112
113
  records.select do |record|
113
- options.map {|key, value| record[key.to_s] == value}.all?
114
+ options.map {|key, value| record.attributes[key.to_s] == value}.all?
114
115
  end
115
116
  when :first
116
117
  return records.first if options.empty?
117
118
  records.detect do |record|
118
- options.map {|key, value| record[key.to_s] == value}.all?
119
+ options.map {|key, value| record.attributes[key.to_s] == value}.all?
119
120
  end
120
121
  end
121
122
  end
@@ -130,13 +131,13 @@ module DBF
130
131
  # Returns a database schema in the portable ActiveRecord::Schema format.
131
132
  #
132
133
  # xBase data types are converted to generic types as follows:
133
- # - Number fields are converted to :integer if there are no decimals, otherwise
134
+ # - Number columns are converted to :integer if there are no decimals, otherwise
134
135
  # they are converted to :float
135
- # - Date fields are converted to :datetime
136
- # - Logical fields are converted to :boolean
137
- # - Memo fields are converted to :text
138
- # - Character fields are converted to :string and the :limit option is set
139
- # to the length of the character field
136
+ # - Date columns are converted to :datetime
137
+ # - Logical columns are converted to :boolean
138
+ # - Memo columns are converted to :text
139
+ # - Character columns are converted to :string and the :limit option is set
140
+ # to the length of the character column
140
141
  #
141
142
  # Example:
142
143
  # create_table "mydata" do |t|
@@ -148,12 +149,12 @@ module DBF
148
149
  # end
149
150
  def schema(path = nil)
150
151
  s = "ActiveRecord::Schema.define do\n"
151
- s << " create_table \"#{File.basename(@data_file.path, ".*")}\" do |t|\n"
152
- fields.each do |field|
153
- s << " t.column \"#{field.name}\""
154
- case field.type
152
+ s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
153
+ columns.each do |column|
154
+ s << " t.column \"#{column.name}\""
155
+ case column.type
155
156
  when "N" # number
156
- if field.decimal > 0
157
+ if column.decimal > 0
157
158
  s << ", :float"
158
159
  else
159
160
  s << ", :integer"
@@ -165,17 +166,17 @@ module DBF
165
166
  when "M" # memo
166
167
  s << ", :text"
167
168
  else
168
- s << ", :string, :limit => #{field.length}"
169
+ s << ", :string, :limit => #{column.length}"
169
170
  end
170
171
  s << "\n"
171
172
  end
172
173
  s << " end\nend"
173
174
 
174
175
  if path
175
- return File.open(path, 'w') {|f| f.puts(s)}
176
+ File.open(path, 'w') {|f| f.puts(s)}
176
177
  else
177
- return s
178
- end
178
+ s
179
+ end
179
180
  end
180
181
 
181
182
  private
@@ -191,45 +192,42 @@ module DBF
191
192
  nil
192
193
  end
193
194
 
194
- # Returns false if the record has been marked as deleted, otherwise it returns true. When dBase records are deleted a
195
- # flag is set, marking the record as deleted. The record will not be fully removed until the database has been compacted.
196
- def active_record?
197
- @data_file.read(1).unpack('H2').to_s == '20'
198
- rescue
199
- false
195
+ def deleted_record?
196
+ @data.read(1).unpack('a') == ['*']
200
197
  end
201
198
 
202
199
  def get_header_info
203
- @data_file.rewind
204
- @version, @record_count, @header_length, @record_length = @data_file.read(DBF_HEADER_SIZE).unpack('H2xxxVvv')
205
- @field_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
200
+ @data.rewind
201
+ @version, @record_count, @header_length, @record_length = @data.read(DBF_HEADER_SIZE).unpack('H2 x3 V v2')
202
+ @column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
206
203
  end
207
204
 
208
- def get_field_descriptors
209
- @fields = []
210
- @field_count.times do
211
- name, type, length, decimal = @data_file.read(32).unpack('a10xax4CC')
212
- if length > 0 && !name.strip.empty?
213
- @fields << Field.new(name, type, length, decimal)
205
+ def get_column_descriptors
206
+ @columns = []
207
+ @column_count.times do
208
+ name, type, length, decimal = @data.read(32).unpack('a10 x a x4 C2')
209
+ if length > 0 && name.strip.any?
210
+ @columns << Column.new(name, type, length, decimal)
214
211
  end
215
212
  end
216
- # adjust field count
217
- @field_count = @fields.size
218
- @fields
213
+ # Reset the column count
214
+ @column_count = @columns.size
215
+
216
+ @columns
219
217
  end
220
218
 
221
219
  def get_memo_header_info
222
- @memo_file.rewind
220
+ @memo.rewind
223
221
  if @memo_file_format == :fpt
224
- @memo_next_available_block, @memo_block_size = @memo_file.read(FPT_HEADER_SIZE).unpack('Nxxn')
222
+ @memo_next_available_block, @memo_block_size = @memo.read(FPT_HEADER_SIZE).unpack('N x2 n')
225
223
  else
226
224
  @memo_block_size = 512
227
- @memo_next_available_block = File.size(@memo_file.path) / @memo_block_size
225
+ @memo_next_available_block = File.size(@memo.path) / @memo_block_size
228
226
  end
229
227
  end
230
228
 
231
229
  def seek(offset)
232
- @data_file.seek(@header_length + offset)
230
+ @data.seek(@header_length + offset)
233
231
  end
234
232
 
235
233
  def seek_to_record(index)
@@ -241,16 +239,14 @@ module DBF
241
239
  # information on how these two methods differ.
242
240
  def get_record_from_file(index)
243
241
  seek_to_record(@db_index[index])
244
- active_record? ? Record.new(self, @data_file, @memo_file) : nil
242
+ deleted_record? ? nil : Record.new(self)
245
243
  end
246
244
 
247
245
  def get_all_records_from_file
248
246
  all_records = []
249
247
  0.upto(@record_count - 1) do |n|
250
248
  seek_to_record(n)
251
- if active_record?
252
- all_records << DBF::Record.new(self, @data_file, @memo_file)
253
- end
249
+ all_records << DBF::Record.new(self) unless deleted_record?
254
250
  end
255
251
  all_records
256
252
  end
@@ -260,10 +256,10 @@ module DBF
260
256
  @deleted_records = []
261
257
  0.upto(@record_count - 1) do |n|
262
258
  seek_to_record(n)
263
- if active_record?
264
- @db_index << n
265
- else
259
+ if deleted_record?
266
260
  @deleted_records << n
261
+ else
262
+ @db_index << n
267
263
  end
268
264
  end
269
265
  end