infused-dbf 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/dbf/table.rb ADDED
@@ -0,0 +1,243 @@
1
+ module DBF
2
+
3
+ class Table
4
+ include Enumerable
5
+
6
+ attr_reader :column_count # The total number of columns (columns)
7
+ attr_reader :columns # An array of DBF::Column
8
+ attr_reader :version # Internal dBase version number
9
+ attr_reader :last_updated # Last updated datetime
10
+ attr_reader :memo_file_format # :fpt or :dpt
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
13
+ attr_reader :data # DBF file handle
14
+ attr_reader :memo # Memo file handle
15
+
16
+ # Initializes a new DBF::Table
17
+ # Example:
18
+ # 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
23
+ reload!
24
+ end
25
+
26
+ # Reloads the database and memo files
27
+ def reload!
28
+ @records = nil
29
+ get_header_info
30
+ get_memo_header_info if @memo
31
+ get_column_descriptors
32
+ build_db_index
33
+ end
34
+
35
+ # Returns true if there is a corresponding memo file
36
+ def has_memo_file?
37
+ @memo ? true : false
38
+ end
39
+
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.
47
+ def column(column_name)
48
+ @columns.detect {|f| f.name == column_name.to_s}
49
+ end
50
+
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
+
59
+ def each
60
+ 0.upto(@record_count - 1) do |n|
61
+ seek_to_record(n)
62
+ unless deleted_record?
63
+ yield DBF::Record.new(self)
64
+ end
65
+ end
66
+ end
67
+
68
+ # def get_record_from_file(index)
69
+ # seek_to_record(@db_index[index])
70
+ # Record.new(self)
71
+ # end
72
+
73
+ # Returns a DBF::Record (or nil if the record has been marked for deletion) for the record at <tt>index</tt>.
74
+ def record(index)
75
+ records[index]
76
+ end
77
+
78
+ # Find records using a simple ActiveRecord-like syntax.
79
+ #
80
+ # Examples:
81
+ # table = DBF::Table.new 'mydata.dbf'
82
+ #
83
+ # # Find record number 5
84
+ # table.find(5)
85
+ #
86
+ # # Find all records for Keith Morrison
87
+ # table.find :all, :first_name => "Keith", :last_name => "Morrison"
88
+ #
89
+ # # Find first record
90
+ # table.find :first, :first_name => "Keith"
91
+ #
92
+ # The <b>command</b> can be an id, :all, or :first.
93
+ # <b>options</b> is optional and, if specified, should be a hash where the keys correspond
94
+ # to column names in the database. The values will be matched exactly with the value
95
+ # in the database. If you specify more than one key, all values must match in order
96
+ # for the record to be returned. The equivalent SQL would be "WHERE key1 = 'value1'
97
+ # AND key2 = 'value2'".
98
+ def find(command, options = {})
99
+ results = options.empty? ? records : records.select {|record| all_values_match?(record, options)}
100
+
101
+ case command
102
+ when Fixnum
103
+ record(command)
104
+ when :all
105
+ results
106
+ when :first
107
+ results.first
108
+ end
109
+ end
110
+
111
+ alias_method :row, :record
112
+
113
+ # Returns a description of the current database file.
114
+ def version_description
115
+ VERSION_DESCRIPTIONS[version]
116
+ end
117
+
118
+ # Returns a database schema in the portable ActiveRecord::Schema format.
119
+ #
120
+ # xBase data types are converted to generic types as follows:
121
+ # - Number columns are converted to :integer if there are no decimals, otherwise
122
+ # they are converted to :float
123
+ # - Date columns are converted to :datetime
124
+ # - Logical columns are converted to :boolean
125
+ # - Memo columns are converted to :text
126
+ # - Character columns are converted to :string and the :limit option is set
127
+ # to the length of the character column
128
+ #
129
+ # Example:
130
+ # create_table "mydata" do |t|
131
+ # t.column :name, :string, :limit => 30
132
+ # t.column :last_update, :datetime
133
+ # t.column :is_active, :boolean
134
+ # t.column :age, :integer
135
+ # t.column :notes, :text
136
+ # end
137
+ def schema(path = nil)
138
+ s = "ActiveRecord::Schema.define do\n"
139
+ s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
140
+ columns.each do |column|
141
+ s << " t.column #{column.schema_definition}"
142
+ end
143
+ s << " end\nend"
144
+
145
+ if path
146
+ File.open(path, 'w') {|f| f.puts(s)}
147
+ else
148
+ s
149
+ end
150
+ end
151
+
152
+ # Returns the record at <tt>index</tt> by seeking to the record in the
153
+ # physical database file. See the documentation for the records method for
154
+ # information on how these two methods differ.
155
+ def get_record_from_file(index)
156
+ seek_to_record(@db_index[index])
157
+ Record.new(self)
158
+ end
159
+
160
+ private
161
+
162
+ def open_memo(file)
163
+ %w(fpt FPT dbt DBT).each do |extname|
164
+ filename = replace_extname(file, extname)
165
+ if File.exists?(filename)
166
+ @memo_file_format = extname.downcase.to_sym
167
+ return File.open(filename, 'rb')
168
+ end
169
+ end
170
+ nil
171
+ end
172
+
173
+ def replace_extname(filename, extension)
174
+ filename.sub(/#{File.extname(filename)[1..-1]}$/, extension)
175
+ end
176
+
177
+ def deleted_record?
178
+ if @data.read(1).unpack('a') == ['*']
179
+ @data.rewind
180
+ true
181
+ else
182
+ false
183
+ end
184
+ end
185
+
186
+ def get_header_info
187
+ @data.rewind
188
+ @version, @record_count, @header_length, @record_length = @data.read(DBF_HEADER_SIZE).unpack('H2 x3 V v2')
189
+ @column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
190
+ end
191
+
192
+ def get_column_descriptors
193
+ @columns = []
194
+ @column_count.times do
195
+ name, type, length, decimal = @data.read(32).unpack('a10 x a x4 C2')
196
+ if length > 0
197
+ @columns << Column.new(name.strip, type, length, decimal)
198
+ end
199
+ end
200
+ # Reset the column count in case any were skipped
201
+ @column_count = @columns.size
202
+
203
+ @columns
204
+ end
205
+
206
+ def get_memo_header_info
207
+ @memo.rewind
208
+ if @memo_file_format == :fpt
209
+ @memo_next_available_block, @memo_block_size = @memo.read(FPT_HEADER_SIZE).unpack('N x2 n')
210
+ @memo_block_size = 0 if @memo_block_size.nil?
211
+ else
212
+ @memo_block_size = 512
213
+ @memo_next_available_block = File.size(@memo.path) / @memo_block_size
214
+ end
215
+ end
216
+
217
+ def seek(offset)
218
+ @data.seek(@header_length + offset)
219
+ end
220
+
221
+ def seek_to_record(index)
222
+ seek(index * @record_length)
223
+ end
224
+
225
+ def build_db_index
226
+ @db_index = []
227
+ @deleted_records = []
228
+ 0.upto(@record_count - 1) do |n|
229
+ seek_to_record(n)
230
+ if deleted_record?
231
+ @deleted_records << n
232
+ else
233
+ @db_index << n
234
+ end
235
+ end
236
+ end
237
+
238
+ def all_values_match?(record, options)
239
+ options.map {|key, value| record.attributes[key.to_s.underscore] == value}.all?
240
+ end
241
+ end
242
+
243
+ end
data/lib/dbf.rb ADDED
@@ -0,0 +1,7 @@
1
+ require 'date'
2
+ require 'activesupport'
3
+
4
+ require 'dbf/globals'
5
+ require 'dbf/record'
6
+ require 'dbf/column'
7
+ require 'dbf/table'
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,19 @@
1
+ ActiveRecord::Schema.define do
2
+ create_table "dbase_83" do |t|
3
+ t.column "id", :integer
4
+ t.column "catcount", :integer
5
+ t.column "agrpcount", :integer
6
+ t.column "pgrpcount", :integer
7
+ t.column "order", :integer
8
+ t.column "code", :string, :limit => 50
9
+ t.column "name", :string, :limit => 100
10
+ t.column "thumbnail", :string, :limit => 254
11
+ t.column "image", :string, :limit => 254
12
+ t.column "price", :float
13
+ t.column "cost", :float
14
+ t.column "desc", :text
15
+ t.column "weight", :float
16
+ t.column "taxable", :boolean
17
+ t.column "active", :boolean
18
+ end
19
+ end
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,45 @@
1
+ describe DBF, :shared => true do
2
+ specify "sum of column lengths should equal record length specified in header" do
3
+ header_record_length = @table.instance_eval {@record_length}
4
+ sum_of_column_lengths = @table.columns.inject(1) {|sum, column| sum + column.length}
5
+
6
+ header_record_length.should == sum_of_column_lengths
7
+ end
8
+
9
+ specify "records should be instances of DBF::Record" do
10
+ @table.records.all? {|record| record.should be_an_instance_of(DBF::Record)}
11
+ end
12
+
13
+ specify "columns should be instances of DBF::Column" do
14
+ @table.columns.all? {|column| column.should be_an_instance_of(DBF::Column)}
15
+ end
16
+
17
+ specify "column names should not be blank" do
18
+ @table.columns.all? {|column| column.name.should_not be_empty}
19
+ end
20
+
21
+ specify "column types should be valid" do
22
+ valid_column_types = %w(C N L D M F B G P Y T I V X @ O +)
23
+ @table.columns.all? {|column| valid_column_types.should include(column.type)}
24
+ end
25
+
26
+ specify "column lengths should be instances of Fixnum" do
27
+ @table.columns.all? {|column| column.length.should be_an_instance_of(Fixnum)}
28
+ end
29
+
30
+ specify "column lengths should be larger than 0" do
31
+ @table.columns.all? {|column| column.length.should > 0}
32
+ end
33
+
34
+ specify "column decimals should be instances of Fixnum" do
35
+ @table.columns.all? {|column| column.decimal.should be_an_instance_of(Fixnum)}
36
+ end
37
+
38
+ specify "column read accessors should return the attribute after typecast" do
39
+ @table.columns do |column|
40
+ record = table.records.first
41
+ record.send(column.name).should == record[column.name]
42
+ end
43
+ end
44
+
45
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + "/../spec_helper"
2
+ require File.dirname(__FILE__) + "/dbf_shared"
3
+
4
+ describe DBF, "of type 03 (dBase III without memo file)" do
5
+ before(:each) do
6
+ @table = DBF::Table.new "#{DB_PATH}/dbase_03.dbf"
7
+ end
8
+
9
+ it_should_behave_like "DBF"
10
+
11
+ it "should report the correct version number" do
12
+ @table.version.should == "03"
13
+ end
14
+
15
+ it "should have a memo file" do
16
+ @table.should_not have_memo_file
17
+ end
18
+
19
+ it "should report the correct memo type" do
20
+ @table.memo_file_format.should be_nil
21
+ end
22
+
23
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + "/../spec_helper"
2
+ require File.dirname(__FILE__) + "/dbf_shared"
3
+
4
+ describe DBF, "of type 30 (Visual FoxPro)" do
5
+ before(:each) do
6
+ @table = DBF::Table.new "#{DB_PATH}/dbase_30.dbf"
7
+ end
8
+
9
+ it_should_behave_like "DBF"
10
+
11
+ it "should report the correct version number" do
12
+ @table.version.should == "30"
13
+ end
14
+
15
+ it "should have a memo file" do
16
+ @table.should have_memo_file
17
+ end
18
+
19
+ it "should report the correct memo type" do
20
+ @table.memo_file_format.should == :fpt
21
+ end
22
+
23
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + "/../spec_helper"
2
+ require File.dirname(__FILE__) + "/dbf_shared"
3
+
4
+ describe DBF, "of type 83 (dBase III with memo file)" do
5
+ before(:each) do
6
+ @table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
7
+ end
8
+
9
+ it_should_behave_like "DBF"
10
+
11
+ it "should report the correct version number" do
12
+ @table.version.should == "83"
13
+ end
14
+
15
+ it "should have a memo file" do
16
+ @table.should have_memo_file
17
+ end
18
+
19
+ it "should report the correct memo type" do
20
+ @table.memo_file_format.should == :dbt
21
+ end
22
+
23
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + "/../spec_helper"
2
+ require File.dirname(__FILE__) + "/dbf_shared"
3
+
4
+ describe DBF, "of type 8b (dBase IV with memo file)" do
5
+ before(:each) do
6
+ @table = DBF::Table.new "#{DB_PATH}/dbase_8b.dbf"
7
+ end
8
+
9
+ it_should_behave_like "DBF"
10
+
11
+ it "should report the correct version number" do
12
+ @table.version.should == "8b"
13
+ end
14
+
15
+ it "should have a memo file" do
16
+ @table.should have_memo_file
17
+ end
18
+
19
+ it "should report the correct memo type" do
20
+ @table.memo_file_format.should == :dbt
21
+ end
22
+
23
+ end
@@ -0,0 +1,23 @@
1
+ require File.dirname(__FILE__) + "/../spec_helper"
2
+ require File.dirname(__FILE__) + "/dbf_shared"
3
+
4
+ describe DBF, "of type f5 (FoxPro with memo file)" do
5
+ before(:each) do
6
+ @table = DBF::Table.new "#{DB_PATH}/dbase_f5.dbf"
7
+ end
8
+
9
+ it_should_behave_like "DBF"
10
+
11
+ it "should report the correct version number" do
12
+ @table.version.should == "f5"
13
+ end
14
+
15
+ it "should have a memo file" do
16
+ @table.should have_memo_file
17
+ end
18
+
19
+ it "should report the correct memo type" do
20
+ @table.memo_file_format.should == :fpt
21
+ end
22
+
23
+ end
@@ -0,0 +1,10 @@
1
+ $:.unshift(File.dirname(__FILE__) + "/../lib/")
2
+ require "rubygems"
3
+ require "spec"
4
+ require "dbf"
5
+
6
+ DB_PATH = File.dirname(__FILE__) + '/fixtures' unless defined?(DB_PATH)
7
+
8
+ Spec::Runner.configure do |config|
9
+
10
+ end
@@ -0,0 +1,124 @@
1
+ require File.dirname(__FILE__) + "/../spec_helper"
2
+
3
+ describe DBF::Column do
4
+
5
+ context "when initialized" do
6
+ before do
7
+ @column = DBF::Column.new "ColumnName", "N", 1, 0
8
+ end
9
+
10
+ it "should set the #name accessor" do
11
+ @column.name.should == "ColumnName"
12
+ end
13
+
14
+ it "should set the #type accessor" do
15
+ @column.type.should == "N"
16
+ end
17
+
18
+ it "should set the #length accessor" do
19
+ @column.length.should == 1
20
+ end
21
+
22
+ it "should set the #decimal accessor" do
23
+ @column.decimal.should == 0
24
+ end
25
+
26
+ it "should raise an error if length is greater than 0" do
27
+ lambda { column = DBF::Column.new "ColumnName", "N", -1, 0 }.should raise_error(DBF::ColumnLengthError)
28
+ end
29
+ end
30
+
31
+ context "#type_cast" do
32
+ it "should cast numbers with decimals to Float" do
33
+ value = "13.5"
34
+ column = DBF::Column.new "ColumnName", "N", 2, 1
35
+ column.type_cast(value).should == 13.5
36
+ end
37
+
38
+ it "should cast numbers with no decimals to Integer" do
39
+ value = "135"
40
+ column = DBF::Column.new "ColumnName", "N", 3, 0
41
+ column.type_cast(value).should == 13105
42
+ end
43
+
44
+ it "should cast :integer to Integer" do
45
+ value = "135"
46
+ column = DBF::Column.new "ColumnName", "I", 3, 0
47
+ column.type_cast(value).should == 13105
48
+ end
49
+
50
+ it "should cast boolean to True" do
51
+ value = "y"
52
+ column = DBF::Column.new "ColumnName", "L", 1, 0
53
+ column.type_cast(value).should == true
54
+ end
55
+
56
+ it "should cast boolean to False" do
57
+ value = "n"
58
+ column = DBF::Column.new "ColumnName", "L", 1, 0
59
+ column.type_cast(value).should == false
60
+ end
61
+
62
+ it "should cast :datetime columns to DateTime" do
63
+ value = "Nl%\000\300Z\252\003"
64
+ column = DBF::Column.new "ColumnName", "T", 16, 0
65
+ column.type_cast(value).should == "2002-10-10T17:04:56+00:00"
66
+ end
67
+
68
+ it "should cast :date columns to Date" do
69
+ value = "20050712"
70
+ column = DBF::Column.new "ColumnName", "D", 8, 0
71
+ column.type_cast(value).should == Date.new(2005,7,12)
72
+ end
73
+ end
74
+
75
+ context "#schema_definition" do
76
+ it "should define an integer column if type is (N)umber with 9 decimals" do
77
+ column = DBF::Column.new "ColumnName", "N", 1, 0
78
+ column.schema_definition.should == "\"column_name\", :integer\n"
79
+ end
80
+
81
+ it "should define a float colmn if type is (N)umber with more than 0 decimals" do
82
+ column = DBF::Column.new "ColumnName", "N", 1, 2
83
+ column.schema_definition.should == "\"column_name\", :float\n"
84
+ end
85
+
86
+ it "should define a date column if type is (D)ate" do
87
+ column = DBF::Column.new "ColumnName", "D", 8, 0
88
+ column.schema_definition.should == "\"column_name\", :date\n"
89
+ end
90
+
91
+ it "should define a datetime column if type is (D)ate" do
92
+ column = DBF::Column.new "ColumnName", "T", 16, 0
93
+ column.schema_definition.should == "\"column_name\", :datetime\n"
94
+ end
95
+
96
+ it "should define a boolean column if type is (L)ogical" do
97
+ column = DBF::Column.new "ColumnName", "L", 1, 0
98
+ column.schema_definition.should == "\"column_name\", :boolean\n"
99
+ end
100
+
101
+ it "should define a text column if type is (M)emo" do
102
+ column = DBF::Column.new "ColumnName", "M", 1, 0
103
+ column.schema_definition.should == "\"column_name\", :text\n"
104
+ end
105
+
106
+ it "should define a string column with length for any other data types" do
107
+ column = DBF::Column.new "ColumnName", "X", 20, 0
108
+ column.schema_definition.should == "\"column_name\", :string, :limit => 20\n"
109
+ end
110
+ end
111
+
112
+ context "#strip_non_ascii_chars" do
113
+ it "should strip characters below decimal 32 and above decimal 127" do
114
+ column = DBF::Column.new "ColumnName", "N", 1, 0
115
+ column.send(:strip_non_ascii_chars, "--\x1F-\x68\x65\x6C\x6C\x6F world-\x80--").should == "---hello world---"
116
+ end
117
+
118
+ it "should truncate characters with decimal 0" do
119
+ column = DBF::Column.new "ColumnName", "N", 1, 0
120
+ column.send(:strip_non_ascii_chars, "--\x1F-\x68\x65\x6C\x6C\x6F \x00 world-\x80--").should == "---hello "
121
+ end
122
+ end
123
+
124
+ end