dbf 2.0.7 → 2.0.8
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/CHANGELOG.md +3 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +250 -13
- data/Gemfile.travis +9 -0
- data/Gemfile.travis18 +8 -0
- data/Guardfile +8 -0
- data/{MIT-LICENSE → LICENSE} +1 -1
- data/README.md +74 -49
- data/Rakefile +0 -7
- data/dbf.gemspec +1 -12
- data/lib/dbf.rb +1 -0
- data/lib/dbf/column/base.rb +48 -25
- data/lib/dbf/column/foxpro.rb +4 -4
- data/lib/dbf/header.rb +5 -1
- data/lib/dbf/memo/base.rb +2 -2
- data/lib/dbf/record.rb +17 -8
- data/lib/dbf/schema.rb +34 -0
- data/lib/dbf/table.rb +55 -55
- data/lib/dbf/version.rb +2 -2
- data/spec/dbf/column_spec.rb +39 -29
- data/spec/dbf/file_formats_spec.rb +28 -14
- data/spec/dbf/record_spec.rb +35 -34
- data/spec/dbf/table_spec.rb +46 -47
- data/spec/spec_helper.rb +10 -0
- metadata +17 -37
- checksums.yaml +0 -7
data/Rakefile
CHANGED
@@ -1,4 +1,3 @@
|
|
1
|
-
require 'rubygems'
|
2
1
|
require 'bundler/setup';
|
3
2
|
Bundler.setup(:default, :development)
|
4
3
|
|
@@ -17,9 +16,3 @@ desc "Open an irb session preloaded with this library"
|
|
17
16
|
task :console do
|
18
17
|
sh "irb -rubygems -I lib -r dbf.rb"
|
19
18
|
end
|
20
|
-
|
21
|
-
# require 'metric_fu'
|
22
|
-
# MetricFu::Configuration.run do |config|
|
23
|
-
# config.rcov[:test_files] = ['spec/**/*_spec.rb']
|
24
|
-
# config.rcov[:rcov_opts] << "-Ispec"
|
25
|
-
# end
|
data/dbf.gemspec
CHANGED
@@ -14,22 +14,11 @@ Gem::Specification.new do |s|
|
|
14
14
|
|
15
15
|
s.executables = ['dbf']
|
16
16
|
s.rdoc_options = ['--charset=UTF-8']
|
17
|
-
s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', '
|
17
|
+
s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'LICENSE']
|
18
18
|
s.files = Dir['[A-Z]*', '{bin,docs,lib,spec}/**/*', 'dbf.gemspec']
|
19
19
|
s.test_files = Dir.glob('spec/**/*_spec.rb')
|
20
20
|
s.require_paths = ['lib']
|
21
21
|
|
22
22
|
s.required_rubygems_version = '>= 1.3.0'
|
23
23
|
s.add_dependency 'fastercsv', '~> 1.5.4'
|
24
|
-
|
25
|
-
s.add_development_dependency 'rspec'
|
26
|
-
s.add_development_dependency 'rake', '>= 0.9.2'
|
27
|
-
|
28
|
-
# if RUBY_VERSION.to_f >= 1.9
|
29
|
-
# s.add_development_dependency 'ruby-debug19'
|
30
|
-
# elsif RUBY_VERSION != '1.8.6'
|
31
|
-
# s.add_development_dependency 'ruby-debug'
|
32
|
-
# end
|
33
|
-
# s.add_development_dependency 'metric_fu'
|
34
24
|
end
|
35
|
-
|
data/lib/dbf.rb
CHANGED
data/lib/dbf/column/base.rb
CHANGED
@@ -7,7 +7,7 @@ module DBF
|
|
7
7
|
class NameError < StandardError; end
|
8
8
|
|
9
9
|
class Base
|
10
|
-
attr_reader :name, :type, :length, :decimal
|
10
|
+
attr_reader :table, :name, :type, :length, :decimal
|
11
11
|
|
12
12
|
# Initialize a new DBF::Column
|
13
13
|
#
|
@@ -15,11 +15,22 @@ module DBF
|
|
15
15
|
# @param [String] type
|
16
16
|
# @param [Fixnum] length
|
17
17
|
# @param [Fixnum] decimal
|
18
|
-
def initialize(name, type, length, decimal
|
19
|
-
@
|
18
|
+
def initialize(table, name, type, length, decimal)
|
19
|
+
@table = table
|
20
|
+
@name = clean(name)
|
21
|
+
@type = type
|
22
|
+
@length = length
|
23
|
+
@decimal = decimal
|
24
|
+
@version = table.version
|
25
|
+
@encoding = table.encoding
|
26
|
+
|
27
|
+
unless length > 0
|
28
|
+
raise LengthError, "field length must be greater than 0"
|
29
|
+
end
|
20
30
|
|
21
|
-
|
22
|
-
|
31
|
+
if @name.empty?
|
32
|
+
raise NameError, "column name cannot be empty"
|
33
|
+
end
|
23
34
|
end
|
24
35
|
|
25
36
|
# Cast value to native type
|
@@ -31,7 +42,7 @@ module DBF
|
|
31
42
|
when 'N' then unpack_number(value)
|
32
43
|
when 'I' then unpack_unsigned_long(value)
|
33
44
|
when 'F' then value.to_f
|
34
|
-
when 'Y' then unpack_unsigned_long(value) / 10000.0
|
45
|
+
when 'Y' then (unpack_unsigned_long(value) / 10000.0).to_f
|
35
46
|
when 'D' then decode_date(value)
|
36
47
|
when 'T' then decode_datetime(value)
|
37
48
|
when 'L' then boolean(value)
|
@@ -40,6 +51,9 @@ module DBF
|
|
40
51
|
end
|
41
52
|
end
|
42
53
|
|
54
|
+
# Returns true if the column is a memo
|
55
|
+
#
|
56
|
+
# @return [Boolean]
|
43
57
|
def memo?
|
44
58
|
@memo ||= type == 'M'
|
45
59
|
end
|
@@ -51,16 +65,20 @@ module DBF
|
|
51
65
|
"\"#{underscored_name}\", #{schema_data_type}\n"
|
52
66
|
end
|
53
67
|
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
end
|
61
|
-
|
68
|
+
# Underscored name
|
69
|
+
#
|
70
|
+
# This is the column name converted to underscore format.
|
71
|
+
# For example, MyColumn will be returned as my_column.
|
72
|
+
#
|
73
|
+
# @return [String]
|
62
74
|
def underscored_name
|
63
|
-
@underscored_name ||=
|
75
|
+
@underscored_name ||= begin
|
76
|
+
name.gsub(/::/, '/').
|
77
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
78
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
79
|
+
tr('-', '_').
|
80
|
+
downcase
|
81
|
+
end
|
64
82
|
end
|
65
83
|
|
66
84
|
private
|
@@ -73,9 +91,11 @@ module DBF
|
|
73
91
|
end
|
74
92
|
|
75
93
|
def decode_datetime(value) #nodoc
|
76
|
-
days,
|
77
|
-
|
78
|
-
DateTime.jd(days, (
|
94
|
+
days, msecs = value.unpack('l2')
|
95
|
+
secs = (msecs / 1000).to_i
|
96
|
+
DateTime.jd(days, (secs/3600).to_i, (secs/60).to_i % 60, secs % 60)
|
97
|
+
rescue
|
98
|
+
nil
|
79
99
|
end
|
80
100
|
|
81
101
|
def decode_memo(value) #nodoc
|
@@ -95,10 +115,10 @@ module DBF
|
|
95
115
|
end
|
96
116
|
|
97
117
|
def encode_string(value) #nodoc
|
98
|
-
if @encoding
|
99
|
-
if
|
100
|
-
value.force_encoding(@encoding).encode(
|
101
|
-
|
118
|
+
if @encoding && table.supports_encoding?
|
119
|
+
if table.supports_string_encoding?
|
120
|
+
value.force_encoding(@encoding).encode(*encoding_args)
|
121
|
+
elsif table.supports_iconv?
|
102
122
|
Iconv.conv('UTF-8', @encoding, value)
|
103
123
|
end
|
104
124
|
else
|
@@ -106,6 +126,10 @@ module DBF
|
|
106
126
|
end
|
107
127
|
end
|
108
128
|
|
129
|
+
def encoding_args #nodoc
|
130
|
+
[Encoding.default_external, {:undef => :replace, :invalid => :replace}]
|
131
|
+
end
|
132
|
+
|
109
133
|
def schema_data_type #nodoc
|
110
134
|
case type
|
111
135
|
when "N", "F"
|
@@ -134,9 +158,8 @@ module DBF
|
|
134
158
|
end
|
135
159
|
|
136
160
|
def clean(value) #nodoc
|
137
|
-
|
138
|
-
|
139
|
-
value.gsub(/[^\x20-\x7E]/, "")
|
161
|
+
truncated_value = value.strip.partition("\x00").first
|
162
|
+
truncated_value.gsub(/[^\x20-\x7E]/, '')
|
140
163
|
end
|
141
164
|
|
142
165
|
end
|
data/lib/dbf/column/foxpro.rb
CHANGED
data/lib/dbf/header.rb
CHANGED
@@ -10,8 +10,12 @@ module DBF
|
|
10
10
|
|
11
11
|
def initialize(data, set_encoding)
|
12
12
|
@data = data
|
13
|
-
@version, @record_count, @header_length, @record_length, @encoding_key =
|
13
|
+
@version, @record_count, @header_length, @record_length, @encoding_key = unpack_header
|
14
14
|
@encoding = DBF::ENCODINGS[@encoding_key] if set_encoding
|
15
15
|
end
|
16
|
+
|
17
|
+
def unpack_header
|
18
|
+
data.unpack("H2 x3 V v2 x17H2")
|
19
|
+
end
|
16
20
|
end
|
17
21
|
end
|
data/lib/dbf/memo/base.rb
CHANGED
@@ -5,7 +5,7 @@ module DBF
|
|
5
5
|
BLOCK_SIZE = 512
|
6
6
|
|
7
7
|
def self.open(filename, version)
|
8
|
-
|
8
|
+
new(File.open(filename, 'rb'), version)
|
9
9
|
end
|
10
10
|
|
11
11
|
def initialize(data, version)
|
@@ -45,4 +45,4 @@ module DBF
|
|
45
45
|
end
|
46
46
|
end
|
47
47
|
end
|
48
|
-
end
|
48
|
+
end
|
data/lib/dbf/record.rb
CHANGED
@@ -36,40 +36,49 @@ module DBF
|
|
36
36
|
end
|
37
37
|
|
38
38
|
# Reads attributes by column name
|
39
|
+
#
|
40
|
+
# @param [String, Symbol] key
|
39
41
|
def [](key)
|
40
42
|
key = key.to_s
|
41
43
|
if attributes.has_key?(key)
|
42
44
|
attributes[key]
|
43
|
-
elsif index =
|
45
|
+
elsif index = underscored_column_names.index(key)
|
44
46
|
attributes[@columns[index].name]
|
45
47
|
end
|
46
48
|
end
|
47
49
|
|
50
|
+
# Record attributes
|
51
|
+
#
|
48
52
|
# @return [Hash]
|
49
53
|
def attributes
|
50
54
|
@attributes ||= Hash[@columns.map {|column| [column.name, init_attribute(column)]}]
|
51
55
|
end
|
52
56
|
|
57
|
+
# Overrides standard Object.respond_to? to return true if a
|
58
|
+
# matching column name is found.
|
59
|
+
#
|
60
|
+
# @param [String, Symbol] method
|
61
|
+
# @return [Boolean]
|
53
62
|
def respond_to?(method, *args)
|
54
|
-
if
|
63
|
+
if underscored_column_names.include?(method.to_s)
|
55
64
|
true
|
56
65
|
else
|
57
66
|
super
|
58
67
|
end
|
59
68
|
end
|
60
69
|
|
61
|
-
|
62
|
-
|
70
|
+
private
|
71
|
+
|
72
|
+
def method_missing(method, *args) #nodoc
|
73
|
+
if index = underscored_column_names.index(method.to_s)
|
63
74
|
attributes[@columns[index].name]
|
64
75
|
else
|
65
76
|
super
|
66
77
|
end
|
67
78
|
end
|
68
79
|
|
69
|
-
|
70
|
-
|
71
|
-
def column_names
|
72
|
-
@column_names ||= @columns.map {|column| column.underscored_name}
|
80
|
+
def underscored_column_names # nodoc
|
81
|
+
@underscored_column_names ||= @columns.map {|column| column.underscored_name}
|
73
82
|
end
|
74
83
|
|
75
84
|
def init_attribute(column) #nodoc
|
data/lib/dbf/schema.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module DBF
|
2
|
+
module Schema
|
3
|
+
# Generate an ActiveRecord::Schema
|
4
|
+
#
|
5
|
+
# xBase data types are converted to generic types as follows:
|
6
|
+
# - Number columns with no decimals are converted to :integer
|
7
|
+
# - Number columns with decimals are converted to :float
|
8
|
+
# - Date columns are converted to :datetime
|
9
|
+
# - Logical columns are converted to :boolean
|
10
|
+
# - Memo columns are converted to :text
|
11
|
+
# - Character columns are converted to :string and the :limit option is set
|
12
|
+
# to the length of the character column
|
13
|
+
#
|
14
|
+
# Example:
|
15
|
+
# create_table "mydata" do |t|
|
16
|
+
# t.column :name, :string, :limit => 30
|
17
|
+
# t.column :last_update, :datetime
|
18
|
+
# t.column :is_active, :boolean
|
19
|
+
# t.column :age, :integer
|
20
|
+
# t.column :notes, :text
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# @return [String]
|
24
|
+
def schema
|
25
|
+
s = "ActiveRecord::Schema.define do\n"
|
26
|
+
s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
|
27
|
+
columns.each do |column|
|
28
|
+
s << " t.column #{column.schema_definition}"
|
29
|
+
end
|
30
|
+
s << " end\nend"
|
31
|
+
s
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
data/lib/dbf/table.rb
CHANGED
@@ -1,9 +1,12 @@
|
|
1
1
|
module DBF
|
2
|
+
class FileNotFoundError < StandardError
|
3
|
+
end
|
2
4
|
|
3
5
|
# DBF::Table is the primary interface to a single DBF file and provides
|
4
6
|
# methods for enumerating and searching the records.
|
5
7
|
class Table
|
6
8
|
include Enumerable
|
9
|
+
include Schema
|
7
10
|
|
8
11
|
DBF_HEADER_SIZE = 32
|
9
12
|
|
@@ -59,11 +62,15 @@ module DBF
|
|
59
62
|
# @param [optional String, StringIO] memo Path to the memo file or a StringIO object
|
60
63
|
# @param [optional String, Encoding] encoding Name of the encoding or an Encoding object
|
61
64
|
def initialize(data, memo = nil, encoding = nil)
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
65
|
+
begin
|
66
|
+
@data = open_data(data)
|
67
|
+
@data.rewind
|
68
|
+
@header = Header.new(@data.read(DBF_HEADER_SIZE), supports_encoding?)
|
69
|
+
@encoding = encoding || header.encoding
|
70
|
+
@memo = open_memo(data, memo)
|
71
|
+
rescue Errno::ENOENT => error
|
72
|
+
raise DBF::FileNotFoundError.new("file not found: #{data}")
|
73
|
+
end
|
67
74
|
end
|
68
75
|
|
69
76
|
# @return [TrueClass, FalseClass]
|
@@ -108,7 +115,7 @@ module DBF
|
|
108
115
|
# @param [Fixnum] index
|
109
116
|
# @return [DBF::Record, NilClass]
|
110
117
|
def record(index)
|
111
|
-
|
118
|
+
seek_to_record(index)
|
112
119
|
if !deleted_record?
|
113
120
|
DBF::Record.new(@data.read(header.record_length), columns, version, @memo)
|
114
121
|
end
|
@@ -137,37 +144,6 @@ module DBF
|
|
137
144
|
VERSIONS[version]
|
138
145
|
end
|
139
146
|
|
140
|
-
# Generate an ActiveRecord::Schema
|
141
|
-
#
|
142
|
-
# xBase data types are converted to generic types as follows:
|
143
|
-
# - Number columns with no decimals are converted to :integer
|
144
|
-
# - Number columns with decimals are converted to :float
|
145
|
-
# - Date columns are converted to :datetime
|
146
|
-
# - Logical columns are converted to :boolean
|
147
|
-
# - Memo columns are converted to :text
|
148
|
-
# - Character columns are converted to :string and the :limit option is set
|
149
|
-
# to the length of the character column
|
150
|
-
#
|
151
|
-
# Example:
|
152
|
-
# create_table "mydata" do |t|
|
153
|
-
# t.column :name, :string, :limit => 30
|
154
|
-
# t.column :last_update, :datetime
|
155
|
-
# t.column :is_active, :boolean
|
156
|
-
# t.column :age, :integer
|
157
|
-
# t.column :notes, :text
|
158
|
-
# end
|
159
|
-
#
|
160
|
-
# @return [String]
|
161
|
-
def schema
|
162
|
-
s = "ActiveRecord::Schema.define do\n"
|
163
|
-
s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
|
164
|
-
columns.each do |column|
|
165
|
-
s << " t.column #{column.schema_definition}"
|
166
|
-
end
|
167
|
-
s << " end\nend"
|
168
|
-
s
|
169
|
-
end
|
170
|
-
|
171
147
|
# Dumps all records to a CSV file. If no filename is given then CSV is
|
172
148
|
# output to STDOUT.
|
173
149
|
#
|
@@ -215,38 +191,58 @@ module DBF
|
|
215
191
|
end
|
216
192
|
end
|
217
193
|
|
218
|
-
#
|
194
|
+
# All columns
|
195
|
+
#
|
196
|
+
# @return [Array]
|
219
197
|
def columns
|
220
|
-
@columns ||=
|
221
|
-
@data.seek(DBF_HEADER_SIZE)
|
222
|
-
columns = []
|
223
|
-
while !["\0", "\r"].include?(first_byte = @data.read(1))
|
224
|
-
column_data = first_byte + @data.read(31)
|
225
|
-
name, type, length, decimal = column_data.unpack('a10 x a x4 C2')
|
226
|
-
if length > 0
|
227
|
-
columns << column_class.new(name.strip, type, length, decimal, version, encoding)
|
228
|
-
end
|
229
|
-
end
|
230
|
-
columns
|
231
|
-
end
|
198
|
+
@columns ||= build_columns
|
232
199
|
end
|
233
200
|
|
201
|
+
# Column names
|
202
|
+
#
|
203
|
+
# @return [String]
|
204
|
+
def column_names
|
205
|
+
columns.map { |column| column.name }
|
206
|
+
end
|
207
|
+
|
208
|
+
# Is string encoding supported?
|
209
|
+
# String encoding is always supported in Ruby 1.9+.
|
210
|
+
# Ruby 1.8.x requires that Ruby be compiled with iconv support.
|
234
211
|
def supports_encoding?
|
235
|
-
|
212
|
+
supports_string_encoding? || supports_iconv?
|
213
|
+
end
|
214
|
+
|
215
|
+
# Does String support encoding? Should be true in Ruby 1.9+
|
216
|
+
def supports_string_encoding?
|
217
|
+
''.respond_to?(:encoding)
|
236
218
|
end
|
237
219
|
|
238
|
-
def supports_iconv?
|
220
|
+
def supports_iconv? #nodoc
|
239
221
|
require 'iconv'
|
240
222
|
true
|
241
223
|
rescue
|
242
224
|
false
|
243
225
|
end
|
244
226
|
|
245
|
-
|
246
|
-
|
227
|
+
private
|
228
|
+
|
229
|
+
def build_columns #nodoc
|
230
|
+
columns = []
|
231
|
+
@data.seek(DBF_HEADER_SIZE)
|
232
|
+
while !["\0", "\r"].include?(first_byte = @data.read(1))
|
233
|
+
column_data = first_byte + @data.read(DBF_HEADER_SIZE - 1)
|
234
|
+
name, type, length, decimal = column_data.unpack('a10 x a x4 C2')
|
235
|
+
if length > 0
|
236
|
+
columns << column_class.new(self, name, type, length, decimal)
|
237
|
+
end
|
238
|
+
end
|
239
|
+
columns
|
247
240
|
end
|
248
241
|
|
249
|
-
|
242
|
+
|
243
|
+
def foxpro? #nodoc
|
244
|
+
FOXPRO_VERSIONS.keys.include? version
|
245
|
+
end
|
250
246
|
|
251
247
|
def column_class #nodoc
|
252
248
|
@column_class ||= foxpro? ? Column::Foxpro : Column::Dbase
|
@@ -312,6 +308,10 @@ module DBF
|
|
312
308
|
@data.seek header.header_length + offset
|
313
309
|
end
|
314
310
|
|
311
|
+
def seek_to_record(index) #nodoc
|
312
|
+
seek(index * header.record_length)
|
313
|
+
end
|
314
|
+
|
315
315
|
def csv_class #nodoc
|
316
316
|
@csv_class ||= CSV.const_defined?(:Reader) ? FCSV : CSV
|
317
317
|
end
|