infused-dbf 1.0.7

Sign up to get free protection for your applications and to get access to all the features.
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