dbf 1.0.11 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,10 +1,13 @@
1
+ == 1.0.13
2
+ * Allow passing an array of ids to find
3
+
1
4
  == 1.0.11
2
5
 
3
6
  * Attributes are now accessible by original or underscored name
4
7
 
5
- == 1.0.11
8
+ == 1.1.0
6
9
 
7
- * Minor performance improvements
10
+ * Add support for large table that will not fit into memory
8
11
 
9
12
  == 1.0.9
10
13
 
@@ -11,7 +11,9 @@ DBF is a small fast library for reading dBase, xBase, Clipper and FoxPro databas
11
11
 
12
12
  * No external dependencies
13
13
  * Fields are type cast to the appropriate Ruby types
14
+ * ActiveRecord like finder methods
14
15
  * Ability to dump the database schema in the portable ActiveRecord::Schema format
16
+ * Ruby 1.9.1 compatible
15
17
 
16
18
  ## Installation
17
19
 
@@ -19,32 +21,51 @@ DBF is a small fast library for reading dBase, xBase, Clipper and FoxPro databas
19
21
 
20
22
  ## Basic Usage
21
23
 
24
+ Load a DBF file:
25
+
22
26
  require 'rubygems'
23
27
  require 'dbf'
24
28
 
25
29
  table = DBF::Table.new("widgets.dbf")
26
30
 
27
- # Tables are enumerable
28
- widget_ids = table.map { |row| row.id }
29
- abc_names = table.select { |row| row.name =~ /^[a-cA-C]/ }
30
- sorted = table.sort_by { |row| row.name }
31
-
32
- # Print the 'name' field from record number 4
33
- puts table.record(4).name
31
+ Enumerate all records
34
32
 
35
- # Attributes can also be accessed using the column name as a Hash key
36
- puts table.record(4).attributes["name"]
37
-
38
- # Print the 'name' and 'address' fields from each record
39
33
  table.records.each do |record|
40
34
  puts record.name
41
35
  puts record.email
42
36
  end
37
+
38
+ Load a single record using <tt>records</tt> or <tt>find</tt>
39
+
40
+ table.records(6)
41
+ table.find(6)
43
42
 
44
- # Find records
45
- table.find :all, :first_name => 'Keith'
46
- table.find :all, :first_name => 'Keith', :last_name => 'Morrison'
43
+ Attributes can also be accessed through the attributes hash in original or
44
+ underscored form or as an accessor method using the underscored name.
45
+
46
+ table.record(4).attributes["PhoneBook"]
47
+ table.record(4).attributes["phone_book"]
48
+ table.record(4).phone_book
49
+
50
+ Search for records using a simple hash format. Multiple search criteria are
51
+ ANDed. Use the block form of find if the resulting recordset could be large
52
+ otherwise all records will be loaded into memory.
53
+
54
+ # find all records with first_name equal to Keith
55
+ table.find(:all, :first_name => 'Keith') do |record|
56
+ puts record.last_name
57
+ end
58
+
59
+ # find all records with first_name equal to Keith and last_name equal
60
+ # to Morrison
61
+ table.find(:all, :first_name => 'Keith', :last_name => 'Morrison') do |record|
62
+ puts record.last_name
63
+ end
64
+
65
+ # find the first record with first_name equal to Keith
47
66
  table.find :first, :first_name => 'Keith'
67
+
68
+ # find record number 10
48
69
  table.find(10)
49
70
 
50
71
  ## Migrating to ActiveRecord
@@ -58,7 +79,7 @@ An example of migrating a DBF book table to ActiveRecord using a migration:
58
79
  table = DBF::Table.new('db/dbf/books.dbf')
59
80
  eval(table.schema)
60
81
 
61
- table.records.each do |record|
82
+ table.each do |record|
62
83
  Book.create(record.attributes)
63
84
  end
64
85
  end
@@ -81,7 +102,7 @@ A small command-line utility called dbf is installed along with the gem.
81
102
  ## Limitations and known bugs
82
103
 
83
104
  * DBF is read-only
84
- * Index files are not used
105
+ * External index files are not used
85
106
 
86
107
  ## License
87
108
 
data/Rakefile CHANGED
@@ -27,6 +27,13 @@ Spec::Rake::SpecTask.new :spec do |t|
27
27
  t.spec_files = FileList['spec/**/*spec.rb']
28
28
  end
29
29
 
30
+ desc "Run rcov"
31
+ Spec::Rake::SpecTask.new :rcov do |t|
32
+ t.spec_files = FileList['spec/**/*spec.rb']
33
+ t.rcov = true
34
+ t.rcov_opts = ["--exclude ~\/.gem"]
35
+ end
36
+
30
37
  desc "Run spec docs"
31
38
  Spec::Rake::SpecTask.new :specdoc do |t|
32
39
  t.spec_opts = ["-f specdoc"]
@@ -1,5 +1,5 @@
1
1
  ---
2
2
  :major: 1
3
- :minor: 0
4
- :patch: 11
3
+ :minor: 1
4
+ :patch: 0
5
5
 
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dbf}
8
- s.version = "1.0.11"
8
+ s.version = "1.1.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Keith Morrison"]
12
- s.date = %q{2009-12-05}
12
+ s.date = %q{2009-12-14}
13
13
  s.default_executable = %q{dbf}
14
14
  s.description = %q{A small fast library for reading dBase, xBase, Clipper and FoxPro database files.}
15
15
  s.email = %q{keithm@infused.org}
@@ -2,9 +2,17 @@ module DBF
2
2
  class ColumnLengthError < DBFError; end
3
3
  class ColumnNameError < DBFError; end
4
4
 
5
+ # DBF::Column stores all the information about a column including its name,
6
+ # type, length and number of decimal places (if any)
5
7
  class Column
6
8
  attr_reader :name, :type, :length, :decimal
7
9
 
10
+ # Initialize a new DBF::Column
11
+ #
12
+ # @param [String] name
13
+ # @param [String] type
14
+ # @param [Fixnum] length
15
+ # @param [Fixnum] decimal
8
16
  def initialize(name, type, length, decimal)
9
17
  @name, @type, @length, @decimal = strip_non_ascii_chars(name), type, length, decimal
10
18
 
@@ -12,6 +20,10 @@ module DBF
12
20
  raise ColumnNameError, "column name cannot be empty" if @name.length == 0
13
21
  end
14
22
 
23
+ # Cast value to native type
24
+ #
25
+ # @param [String] value
26
+ # @return [Fixnum, Float, Date, DateTime, Boolean, String]
15
27
  def type_cast(value)
16
28
  case type
17
29
  when 'N' # number
@@ -29,28 +41,50 @@ module DBF
29
41
  end
30
42
  end
31
43
 
44
+ # Decode a DateTime value
45
+ #
46
+ # @param [String] value
47
+ # @return [DateTime]
32
48
  def decode_datetime(value)
33
49
  days, milliseconds = value.unpack('l2')
34
50
  seconds = milliseconds / 1000
35
51
  DateTime.jd(days, seconds/3600, seconds/60 % 60, seconds % 60)
36
52
  end
37
53
 
54
+ # Decode a number value
55
+ #
56
+ # @param [String] value
57
+ # @return [Fixnum, Float]
38
58
  def unpack_number(value)
39
59
  decimal.zero? ? unpack_integer(value) : value.to_f
40
60
  end
41
61
 
62
+ # Decode an integer
63
+ #
64
+ # @param [String] value
65
+ # @return [Fixnum]
42
66
  def unpack_integer(value)
43
67
  value.to_i
44
68
  end
45
69
 
70
+ # Decode a boolean value
71
+ #
72
+ # @param [String] value
73
+ # @return [Boolean]
46
74
  def boolean(value)
47
75
  value.strip =~ /^(y|t)$/i ? true : false
48
76
  end
49
77
 
78
+ # Schema definition
79
+ #
80
+ # @return [String]
50
81
  def schema_definition
51
82
  "\"#{name.underscore}\", #{schema_data_type}\n"
52
83
  end
53
84
 
85
+ # Column type for schema definition
86
+ #
87
+ # @return [String]
54
88
  def schema_data_type
55
89
  case type
56
90
  when "N"
@@ -70,7 +104,10 @@ module DBF
70
104
  end
71
105
  end
72
106
 
73
- # strip all non-ascii and non-printable characters
107
+ # Strip all non-ascii and non-printable characters
108
+ #
109
+ # @param [String] s
110
+ # @return [String]
74
111
  def strip_non_ascii_chars(s)
75
112
  # truncate the string at the first null character
76
113
  s = s[0, s.index("\x00")] if s.index("\x00")
@@ -1,24 +1,40 @@
1
1
  module DBF
2
+
3
+ # An instance of DBF::Record represents a row in the DBF file
2
4
  class Record
3
5
  attr_reader :attributes
6
+ attr_reader :memo_block_size
7
+
4
8
  delegate :columns, :to => :@table
5
9
 
10
+ # Initialize a new DBF::Record
11
+ #
12
+ # @param [DBF::Table] table
6
13
  def initialize(table)
7
14
  @table, @data, @memo = table, table.data, table.memo
15
+ @memo_block_size = @table.memo_block_size
8
16
  initialize_values
9
17
  define_accessors
10
18
  end
11
19
 
20
+ # Equality
21
+ #
22
+ # @param [DBF::Record] other
23
+ # @return [Boolean]
12
24
  def ==(other)
13
25
  other.respond_to?(:attributes) && other.attributes == attributes
14
26
  end
15
27
 
28
+ # Maps a row to an array of values
29
+ #
30
+ # @return [Array]
16
31
  def to_a
17
32
  columns.map { |column| @attributes[column.name.underscore] }
18
33
  end
19
34
 
20
35
  private
21
36
 
37
+ # Defined attribute accessor methods
22
38
  def define_accessors
23
39
  columns.each do |column|
24
40
  underscored_column_name = column.name.underscore
@@ -30,6 +46,7 @@ module DBF
30
46
  end
31
47
  end
32
48
 
49
+ # Initialize values for a row
33
50
  def initialize_values
34
51
  @attributes = columns.inject({}) do |hash, column|
35
52
  if column.type == 'M'
@@ -44,17 +61,27 @@ module DBF
44
61
  hash
45
62
  end
46
63
  end
47
-
64
+
65
+ # Unpack raw data from database
66
+ #
67
+ # @param [Fixnum] length
48
68
  def unpack_data(length)
49
69
  @data.read(length).unpack("a#{length}").first
50
70
  end
51
-
71
+
72
+ # Reads a memo from the memo file
73
+ #
74
+ # @param [Fixnum] start_block
52
75
  def read_memo(start_block)
53
76
  return nil if !@table.has_memo_file? || start_block < 1
54
77
 
55
78
  @table.memo_file_format == :fpt ? build_fpt_memo(start_block) : build_dbt_memo(start_block)
56
79
  end
57
80
 
81
+ # Reconstructs a memo from an FPT memo file
82
+ #
83
+ # @param [Fixnum] start_block
84
+ # @return [String]
58
85
  def build_fpt_memo(start_block)
59
86
  @memo.seek(start_block * memo_block_size)
60
87
 
@@ -69,6 +96,10 @@ module DBF
69
96
  memo_string
70
97
  end
71
98
 
99
+ # Reconstucts a memo from an DBT memo file
100
+ #
101
+ # @param [Fixnum] start_block
102
+ # @return [String]
72
103
  def build_dbt_memo(start_block)
73
104
  @memo.seek(start_block * memo_block_size)
74
105
 
@@ -87,14 +118,16 @@ module DBF
87
118
  memo_string
88
119
  end
89
120
 
90
- def memo_block_size
91
- @memo_block_size ||= @table.memo_block_size
92
- end
93
-
121
+ # The size in bytes of the content for each memo block
122
+ #
123
+ # @return [Fixnum]
94
124
  def memo_block_content_size
95
125
  memo_block_size - BLOCK_HEADER_SIZE
96
126
  end
97
127
 
128
+ # The size in bytes of the entire memo
129
+ #
130
+ # @return [Fixnum]
98
131
  def memo_content_size(memo_size)
99
132
  (memo_size - memo_block_size) + BLOCK_HEADER_SIZE
100
133
  end
@@ -1,120 +1,93 @@
1
1
  module DBF
2
2
 
3
+ # DBF::Table is the primary interface to a single DBF file and provides
4
+ # methods for enumerating and searching the records.
3
5
  class Table
4
- include Enumerable
5
-
6
6
  attr_reader :column_count # The total number of columns
7
7
  attr_reader :columns # An array of DBF::Column
8
8
  attr_reader :version # Internal dBase version number
9
9
  attr_reader :last_updated # Last updated datetime
10
10
  attr_reader :memo_file_format # :fpt or :dpt
11
11
  attr_reader :memo_block_size # The block size for memo records
12
- attr_reader :options # The options hash that was used to initialize the table
12
+ attr_reader :options # The options hash used to initialize the table
13
13
  attr_reader :data # DBF file handle
14
14
  attr_reader :memo # Memo file handle
15
+ attr_reader :record_count # Total number of records
15
16
 
16
- # Initializes a new DBF::Table
17
+ # Opens a DBF::Table
17
18
  # Example:
18
19
  # table = DBF::Table.new 'data.dbf'
19
- def initialize(filename, options = {})
20
- @data = File.open(filename, 'rb')
21
- @memo = open_memo(filename)
22
- @options = options
20
+ #
21
+ # @param [String] path Path to the dbf file
22
+ def initialize(path)
23
+ @data = File.open(path, 'rb')
24
+ @memo = open_memo(path)
23
25
  reload!
24
26
  end
25
27
 
28
+ # Closes the table and memo file
29
+ def close
30
+ @data.close
31
+ @memo.close if @memo
32
+ end
33
+
26
34
  # Reloads the database and memo files
27
35
  def reload!
28
36
  @records = nil
29
37
  get_header_info
30
38
  get_memo_header_info if @memo
31
39
  get_column_descriptors
32
- build_db_index
33
40
  end
34
41
 
35
- # Returns true if there is a corresponding memo file
42
+ # Checks if there is a memo file
43
+ #
44
+ # @return [Boolean]
36
45
  def has_memo_file?
37
46
  @memo ? true : false
38
47
  end
39
48
 
40
- # The total number of active records.
41
- def record_count
42
- @db_index.size
43
- end
44
-
45
- # Returns an instance of DBF::Column for <b>column_name</b>. The <b>column_name</b>
46
- # can be a specified as either a symbol or string.
49
+ # Retrieve a Column by name
50
+ #
51
+ # @param [String, Symbol] column_name
52
+ # @return [DBF::Column]
47
53
  def column(column_name)
48
54
  @columns.detect {|f| f.name == column_name.to_s}
49
55
  end
50
56
 
51
- # An array of all the records contained in the database file. Each record is an instance
52
- # of DBF::Record (or nil if the record is marked for deletion).
53
- def records
54
- self.to_a
55
- end
56
-
57
- alias_method :rows, :records
58
-
57
+ # Calls block once for each record in the table. The record may be nil
58
+ # if the record has been marked as deleted.
59
+ #
60
+ # @yield [nil, DBF::Record]
59
61
  def each
60
62
  0.upto(@record_count - 1) do |n|
61
63
  seek_to_record(n)
62
- unless deleted_record?
63
- yield DBF::Record.new(self)
64
- end
64
+ yield deleted_record? ? nil : DBF::Record.new(self)
65
65
  end
66
66
  end
67
67
 
68
- # Returns a DBF::Record (or nil if the record has been marked for deletion) for the record at <tt>index</tt>.
69
- def record(index)
70
- records[index]
71
- end
72
-
73
- # Find records using a simple ActiveRecord-like syntax.
74
- #
75
- # Examples:
76
- # table = DBF::Table.new 'mydata.dbf'
77
- #
78
- # # Find record number 5
79
- # table.find(5)
80
- #
81
- # # Find all records for Keith Morrison
82
- # table.find :all, :first_name => "Keith", :last_name => "Morrison"
83
- #
84
- # # Find first record
85
- # table.find :first, :first_name => "Keith"
68
+ # Retrieve a record by index number
86
69
  #
87
- # The <b>command</b> can be an id, :all, or :first.
88
- # <b>options</b> is optional and, if specified, should be a hash where the keys correspond
89
- # to column names in the database. The values will be matched exactly with the value
90
- # in the database. If you specify more than one key, all values must match in order
91
- # for the record to be returned. The equivalent SQL would be "WHERE key1 = 'value1'
92
- # AND key2 = 'value2'".
93
- def find(command, options = {})
94
- results = options.empty? ? records : records.select {|record| all_values_match?(record, options)}
95
-
96
- case command
97
- when Fixnum
98
- record(command)
99
- when :all
100
- results
101
- when :first
102
- results.first
103
- end
70
+ # @param [Fixnum] index
71
+ # @return [DBF::Record]
72
+ def record(index)
73
+ seek_to_record(index)
74
+ deleted_record? ? nil : DBF::Record.new(self)
104
75
  end
105
76
 
106
77
  alias_method :row, :record
107
78
 
108
- # Returns a description of the current database file.
79
+ # Human readable version description
80
+ #
81
+ # @return [String]
109
82
  def version_description
110
83
  VERSION_DESCRIPTIONS[version]
111
84
  end
112
85
 
113
- # Returns a database schema in the portable ActiveRecord::Schema format.
86
+ # Generate an ActiveRecord::Schema
114
87
  #
115
88
  # xBase data types are converted to generic types as follows:
116
- # - Number columns are converted to :integer if there are no decimals, otherwise
117
- # they are converted to :float
89
+ # - Number columns with no decimals are converted to :integer
90
+ # - Number columns with decimals are converted to :float
118
91
  # - Date columns are converted to :datetime
119
92
  # - Logical columns are converted to :boolean
120
93
  # - Memo columns are converted to :text
@@ -129,6 +102,9 @@ module DBF
129
102
  # t.column :age, :integer
130
103
  # t.column :notes, :text
131
104
  # end
105
+ #
106
+ # @param [optional String] path
107
+ # @return [String]
132
108
  def schema(path = nil)
133
109
  s = "ActiveRecord::Schema.define do\n"
134
110
  s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
@@ -139,105 +115,181 @@ module DBF
139
115
 
140
116
  if path
141
117
  File.open(path, 'w') {|f| f.puts(s)}
142
- else
143
- s
144
118
  end
119
+
120
+ s
145
121
  end
146
122
 
147
- # Returns the record at <tt>index</tt> by seeking to the record in the
148
- # physical database file. See the documentation for the records method for
149
- # information on how these two methods differ.
150
- def get_record_from_file(index)
151
- seek_to_record(@db_index[index])
152
- Record.new(self)
153
- end
154
-
155
- # Dumps all records into a CSV file
156
- def to_csv(filename = nil)
157
- filename = File.basename(@data.path, '.dbf') + '.csv' if filename.nil?
158
- FCSV.open(filename, 'w', :force_quotes => true) do |csv|
159
- records.each do |record|
123
+ # Dumps all records to a CSV file. If no filename is given then CSV is
124
+ # output to STDOUT.
125
+ #
126
+ # @param [optional String] path Defaults to basename of dbf file
127
+ def to_csv(path = nil)
128
+ path = File.basename(@data.path, '.dbf') + '.csv' if path.nil?
129
+ FCSV.open(path, 'w', :force_quotes => true) do |csv|
130
+ each do |record|
160
131
  csv << record.to_a
161
132
  end
162
133
  end
163
134
  end
164
135
 
136
+ # Find records using a simple ActiveRecord-like syntax.
137
+ #
138
+ # Examples:
139
+ # table = DBF::Table.new 'mydata.dbf'
140
+ #
141
+ # # Find record number 5
142
+ # table.find(5)
143
+ #
144
+ # # Find all records for Keith Morrison
145
+ # table.find :all, :first_name => "Keith", :last_name => "Morrison"
146
+ #
147
+ # # Find first record
148
+ # table.find :first, :first_name => "Keith"
149
+ #
150
+ # The <b>command</b> may be a record index, :all, or :first.
151
+ # <b>options</b> is optional and, if specified, should be a hash where the keys correspond
152
+ # to column names in the database. The values will be matched exactly with the value
153
+ # in the database. If you specify more than one key, all values must match in order
154
+ # for the record to be returned. The equivalent SQL would be "WHERE key1 = 'value1'
155
+ # AND key2 = 'value2'".
156
+ #
157
+ # @param [Fixnum, Symbol] command
158
+ # @param [optional, Hash] options Hash of search parameters
159
+ # @yield [optional, DBF::Record]
160
+ def find(command, options = {}, &block)
161
+ case command
162
+ when Fixnum
163
+ record(command)
164
+ when Array
165
+ command.map {|i| record(i)}
166
+ when :all
167
+ find_all(options, &block)
168
+ when :first
169
+ find_first(options)
170
+ end
171
+ end
172
+
165
173
  private
166
174
 
167
- def open_memo(file)
168
- %w(fpt FPT dbt DBT).each do |extname|
169
- filename = replace_extname(file, extname)
170
- if File.exists?(filename)
171
- @memo_file_format = extname.downcase.to_sym
172
- return File.open(filename, 'rb')
175
+ # Find all matching
176
+ #
177
+ # @param [Hash] options
178
+ # @yield [optional DBF::Record]
179
+ # @return [Array]
180
+ def find_all(options, &block)
181
+ results = []
182
+ each do |record|
183
+ if all_values_match?(record, options)
184
+ if block_given?
185
+ yield(record)
186
+ else
187
+ results << record
173
188
  end
174
189
  end
175
- nil
176
- end
177
-
178
- def replace_extname(filename, extension)
179
- filename.sub(/#{File.extname(filename)[1..-1]}$/, extension)
180
190
  end
191
+ results
192
+ end
181
193
 
182
- def deleted_record?
183
- if @data.read(1).unpack('a') == ['*']
184
- @data.rewind
185
- true
186
- else
187
- false
188
- end
194
+ # Find first matching
195
+ #
196
+ # @param [Hash] options
197
+ # @return [DBF::Record, nil]
198
+ def find_first(options)
199
+ each do |record|
200
+ return record if all_values_match?(record, options)
189
201
  end
202
+ nil
203
+ end
190
204
 
191
- def get_header_info
192
- @data.rewind
193
- @version, @record_count, @header_length, @record_length = @data.read(DBF_HEADER_SIZE).unpack('H2 x3 V v2')
194
- @column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
195
- end
205
+ # Do all search parameters match?
206
+ #
207
+ # @param [DBF::Record] record
208
+ # @param [Hash] options
209
+ # @return [Boolean]
210
+ def all_values_match?(record, options)
211
+ options.all? {|key, value| record.attributes[key.to_s.underscore] == value}
212
+ end
196
213
 
197
- def get_column_descriptors
198
- @columns = []
199
- @column_count.times do
200
- name, type, length, decimal = @data.read(32).unpack('a10 x a x4 C2')
201
- if length > 0
202
- @columns << Column.new(name.strip, type, length, decimal)
203
- end
214
+ # Open memo file
215
+ #
216
+ # @params [String] path
217
+ # @return [File]
218
+ def open_memo(path)
219
+ %w(fpt FPT dbt DBT).each do |extname|
220
+ filename = replace_extname(path, extname)
221
+ if File.exists?(filename)
222
+ @memo_file_format = extname.downcase.to_sym
223
+ return File.open(filename, 'rb')
204
224
  end
205
- # Reset the column count in case any were skipped
206
- @column_count = @columns.size
207
-
208
- @columns
209
225
  end
226
+ nil
227
+ end
210
228
 
211
- def get_memo_header_info
212
- @memo.rewind
213
- if @memo_file_format == :fpt
214
- @memo_next_available_block, @memo_block_size = @memo.read(FPT_HEADER_SIZE).unpack('N x2 n')
215
- @memo_block_size = 0 if @memo_block_size.nil?
216
- else
217
- @memo_block_size = 512
218
- @memo_next_available_block = File.size(@memo.path) / @memo_block_size
219
- end
220
- end
229
+ # Replace the file extension
230
+ #
231
+ # @param [String] path
232
+ # @param [String] extension
233
+ # @return [String]
234
+ def replace_extname(path, extension)
235
+ path.sub(/#{File.extname(path)[1..-1]}$/, extension)
236
+ end
221
237
 
222
- def seek(offset)
223
- @data.seek(@header_length + offset)
224
- end
238
+ # Is record marked for deletion
239
+ #
240
+ # @return [Boolean]
241
+ def deleted_record?
242
+ @data.read(1).unpack('a') == ['*']
243
+ end
225
244
 
226
- def seek_to_record(index)
227
- seek(index * @record_length)
228
- end
229
-
230
- def build_db_index
231
- @db_index = []
232
- 0.upto(@record_count - 1) do |n|
233
- seek_to_record(n)
234
- @db_index << n unless deleted_record?
245
+ # Determine database version, record count, header length and record length
246
+ def get_header_info
247
+ @data.rewind
248
+ @version, @record_count, @header_length, @record_length = @data.read(DBF_HEADER_SIZE).unpack('H2 x3 V v2')
249
+ @column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
250
+ end
251
+
252
+ # Retrieves column information from the database
253
+ def get_column_descriptors
254
+ @columns = []
255
+ @column_count.times do
256
+ name, type, length, decimal = @data.read(32).unpack('a10 x a x4 C2')
257
+ if length > 0
258
+ @columns << Column.new(name.strip, type, length, decimal)
235
259
  end
236
260
  end
261
+ # Reset the column count in case any were skipped
262
+ @column_count = @columns.size
237
263
 
238
- def all_values_match?(record, options)
239
- options.map {|key, value| record.attributes[key.to_s.underscore] == value}.all?
264
+ @columns
265
+ end
266
+
267
+ # Determines the memo block size and next available block
268
+ def get_memo_header_info
269
+ @memo.rewind
270
+ if @memo_file_format == :fpt
271
+ @memo_next_available_block, @memo_block_size = @memo.read(FPT_HEADER_SIZE).unpack('N x2 n')
272
+ @memo_block_size = 0 if @memo_block_size.nil?
273
+ else
274
+ @memo_block_size = 512
275
+ @memo_next_available_block = File.size(@memo.path) / @memo_block_size
240
276
  end
277
+ end
278
+
279
+ # Seek to a byte offset
280
+ #
281
+ # @params [Fixnum] offset
282
+ def seek(offset)
283
+ @data.seek(@header_length + offset)
284
+ end
285
+
286
+ # Seek to a record
287
+ #
288
+ # @param [Fixnum] index
289
+ def seek_to_record(index)
290
+ seek(index * @record_length)
291
+ end
292
+
241
293
  end
242
294
 
243
295
  end
@@ -7,7 +7,9 @@ describe DBF, :shared => true do
7
7
  end
8
8
 
9
9
  specify "records should be instances of DBF::Record" do
10
- @table.records.all? {|record| record.should be_an_instance_of(DBF::Record)}
10
+ @table.each do |record|
11
+ record.should be_an_instance_of(DBF::Record)
12
+ end
11
13
  end
12
14
 
13
15
  specify "columns should be instances of DBF::Column" do
@@ -1,7 +1,8 @@
1
- $:.unshift(File.dirname(__FILE__) + "/../lib/")
2
- require "rubygems"
3
- require "spec"
4
- require "dbf"
1
+ $:.unshift(File.dirname(__FILE__) + '/../lib/')
2
+ require 'rubygems'
3
+ require 'spec'
4
+ require 'dbf'
5
+ require 'fileutils'
5
6
 
6
7
  DB_PATH = File.dirname(__FILE__) + '/fixtures' unless defined?(DB_PATH)
7
8
 
@@ -9,4 +10,4 @@ Spec::Runner.configure do |config|
9
10
 
10
11
  end
11
12
 
12
- self.class.send :remove_const, "Test" if defined? Test
13
+ self.class.send :remove_const, 'Test' if defined? Test
@@ -25,26 +25,26 @@ describe DBF::Record do
25
25
  table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
26
26
  table.column("ID").type.should == "N"
27
27
  table.column("ID").decimal.should == 0
28
- table.records.all? {|record| record.attributes['id'].should be_kind_of(Integer)}
28
+ table.record(1).attributes['id'].should be_kind_of(Integer)
29
29
  end
30
30
 
31
31
  it "should typecast number columns with decimals > 0 to Float" do
32
32
  table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
33
33
  table.column("ID").type.should == "N"
34
34
  table.column("COST").decimal.should == 2
35
- table.records.all? {|record| record.attributes['cost'].should be_kind_of(Float)}
35
+ table.record(1).attributes['cost'].should be_kind_of(Float)
36
36
  end
37
37
 
38
38
  it "should typecast memo columns to String" do
39
39
  table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
40
40
  table.column("DESC").type.should == "M"
41
- table.records.all? {|record| record.attributes['desc'].should be_kind_of(String)}
41
+ table.record(1).attributes['desc'].should be_kind_of(String)
42
42
  end
43
43
 
44
44
  it "should typecast logical columns to True or False" do
45
45
  table = DBF::Table.new "#{DB_PATH}/dbase_30.dbf"
46
46
  table.column("WEBINCLUDE").type.should == "L"
47
- table.records.all? {|record| record.attributes["webinclude"].should satisfy {|v| v == true || v == false}}
47
+ table.record(1).attributes["webinclude"].should satisfy {|v| v == true || v == false}
48
48
  end
49
49
  end
50
50
 
@@ -85,13 +85,13 @@ describe DBF::Record do
85
85
  end
86
86
  end
87
87
 
88
- describe '#to_a' do
89
- it 'should return an ordered array of attribute values' do
90
- table = DBF::Table.new "#{DB_PATH}/dbase_8b.dbf"
91
- record = table.records[9]
92
- record.to_a.should == ["Ten records stored in this database", 10.0, nil, false, "0.100000000000000000", nil]
93
- end
94
- end
88
+ # describe '#to_a' do
89
+ # it 'should return an ordered array of attribute values' do
90
+ # table = DBF::Table.new "#{DB_PATH}/dbase_8b.dbf"
91
+ # record = table.records[9]
92
+ # record.to_a.should == ["Ten records stored in this database", 10.0, nil, false, "0.100000000000000000", nil]
93
+ # end
94
+ # end
95
95
 
96
96
  describe '#==' do
97
97
  before do
@@ -1,8 +1,6 @@
1
1
  require File.dirname(__FILE__) + "/../spec_helper"
2
- require 'fileutils'
3
2
 
4
3
  describe DBF::Table do
5
-
6
4
  context "when initialized" do
7
5
  before do
8
6
  @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
@@ -12,7 +10,7 @@ describe DBF::Table do
12
10
  @table.data.should be_kind_of(File)
13
11
  end
14
12
 
15
- it "should locate load the memo file" do
13
+ it "should load the memo file" do
16
14
  @table.has_memo_file?.should be_true
17
15
  @table.instance_eval("@memo").should be_kind_of(File)
18
16
  end
@@ -36,74 +34,19 @@ describe DBF::Table do
36
34
  it "should determine the database version" do
37
35
  @table.version.should == "83"
38
36
  end
39
-
40
37
  end
41
38
 
42
- describe "#find" do
43
- describe "with index" do
44
- it "should return the correct record" do
45
- table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
46
- table.find(5).should == table.record(5)
47
- end
48
- end
49
-
50
- describe "with :all" do
51
- before do
52
- @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
53
- end
54
-
55
- it "should return all records if options are empty" do
56
- @table.find(:all).should == @table.records
57
- end
58
-
59
- it "should return matching records when used with options" do
60
- @table.find(:all, "WEIGHT" => 0.0).should == @table.select {|r| r.attributes["weight"] == 0.0}
61
- end
62
-
63
- it "with multiple options should search for all search terms as if using AND" do
64
- @table.find(:all, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should == []
65
- end
66
-
67
- it "should match original column names" do
68
- @table.find(:all, "WEIGHT" => 0.0).should_not be_empty
69
- end
70
-
71
- it "should match symbolized column names" do
72
- @table.find(:all, :WEIGHT => 0.0).should_not be_empty
73
- end
74
-
75
- it "should match downcased column names" do
76
- @table.find(:all, "weight" => 0.0).should_not be_empty
77
- end
78
-
79
- it "should match symbolized downcased column names" do
80
- @table.find(:all, :weight => 0.0).should_not be_empty
81
- end
39
+ context "when closed" do
40
+ before do
41
+ @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
82
42
  end
83
-
84
- describe "with :first" do
85
- before do
86
- @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
87
- end
88
-
89
- it "should return the first record if options are empty" do
90
- @table.find(:first).should == @table.records.first
91
- end
92
-
93
- it "should return the first matching record when used with options" do
94
- @table.find(:first, "CODE" => "C").should == @table.record(5)
95
- end
96
-
97
- it "with multiple options should search for all search terms as if using AND" do
98
- @table.find(:first, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should be_nil
99
- end
43
+
44
+ it "should close the data file" do
45
+ @table.close
46
+ lambda { @table.record(1) }.should raise_error(IOError)
100
47
  end
101
48
  end
102
49
 
103
- describe "#reload" do
104
- # TODO
105
- end
106
-
107
50
  describe "#column" do
108
51
  it "should accept a string or symbol as input" do
109
52
  table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
@@ -139,22 +82,9 @@ describe DBF::Table do
139
82
  end
140
83
 
141
84
  describe '#replace_extname' do
142
- it 'should replace the extname' do
85
+ it "should change the file extension" do
143
86
  table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
144
- table.send(:replace_extname, "dbase_83.dbf", 'fpt').should == 'dbase_83.fpt'
145
- end
146
- end
147
-
148
- describe '#each' do
149
- it 'should enumerate all records' do
150
- table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
151
- records = []
152
- table.each do |record|
153
- records << record
154
- end
155
-
156
- records.map! { |r| r.attributes }
157
- records.should == table.records.map {|r| r.attributes}
87
+ table.send(:replace_extname, 'dbase_83.dbf', 'fpt').should == 'dbase_83.fpt'
158
88
  end
159
89
  end
160
90
 
@@ -178,6 +108,108 @@ describe DBF::Table do
178
108
  File.exists?('test.csv').should be_true
179
109
  end
180
110
  end
111
+
112
+ describe "#record" do
113
+ before do
114
+ @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
115
+
116
+ @records = []
117
+ @table.each {|record| @records << record}
118
+ end
119
+
120
+ it "should nullify deleted records" do
121
+ @table.stub!(:deleted_record?).and_return(true)
122
+ @table.record(5).should be_nil
123
+ end
124
+ end
125
+
126
+ describe "#find" do
127
+ describe "with index" do
128
+ before do
129
+ @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
130
+ end
131
+
132
+ it "should return the correct record" do
133
+ @table.find(5).should == @table.record(5)
134
+ end
135
+ end
136
+
137
+ describe 'with array of indexes' do
138
+ before do
139
+ @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
140
+ end
141
+
142
+ it "should return the correct records" do
143
+ @table.find([1, 5, 10]).should == [@table.record(1), @table.record(5), @table.record(10)]
144
+ end
145
+ end
146
+
147
+ describe "with :all" do
148
+ before do
149
+ @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
150
+
151
+ @records = []
152
+ @table.each {|record| @records << record}
153
+ end
154
+
155
+ it "should accept a block" do
156
+ records = []
157
+ @table.find(:all, :weight => 0.0) do |record|
158
+ records << record
159
+ end
160
+ records.should == @table.find(:all, :weight => 0.0)
161
+ end
162
+
163
+ it "should return all records if options are empty" do
164
+ @table.find(:all).should == @records
165
+ end
166
+
167
+ it "should return matching records when used with options" do
168
+ @table.find(:all, "WEIGHT" => 0.0).should == @records.select {|r| r.attributes["weight"] == 0.0}
169
+ end
170
+
171
+ it "should AND multiple search terms" do
172
+ @table.find(:all, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should == []
173
+ end
174
+
175
+ it "should match original column names" do
176
+ @table.find(:all, "WEIGHT" => 0.0).should_not be_empty
177
+ end
178
+
179
+ it "should match symbolized column names" do
180
+ @table.find(:all, :WEIGHT => 0.0).should_not be_empty
181
+ end
182
+
183
+ it "should match downcased column names" do
184
+ @table.find(:all, "weight" => 0.0).should_not be_empty
185
+ end
186
+
187
+ it "should match symbolized downcased column names" do
188
+ @table.find(:all, :weight => 0.0).should_not be_empty
189
+ end
190
+ end
191
+
192
+ describe "with :first" do
193
+ before do
194
+ @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
195
+
196
+ @records = []
197
+ @table.each {|record| @records << record}
198
+ end
199
+
200
+ it "should return the first record if options are empty" do
201
+ @table.find(:first).should == @records.first
202
+ end
203
+
204
+ it "should return the first matching record when used with options" do
205
+ @table.find(:first, "CODE" => "C").should == @records[5]
206
+ end
207
+
208
+ it "should AND multiple search terms" do
209
+ @table.find(:first, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should be_nil
210
+ end
211
+ end
212
+ end
181
213
 
182
214
  end
183
215
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.11
4
+ version: 1.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Morrison
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2009-12-05 00:00:00 -08:00
12
+ date: 2009-12-14 00:00:00 +01:00
13
13
  default_executable: dbf
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency