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