dbf 1.0.11 → 1.1.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.
@@ -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