dbf 2.0.3 → 2.0.4
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 +4 -1
- data/Gemfile +1 -1
- data/Gemfile.lock +15 -17
- data/README.md +28 -24
- data/bin/dbf +3 -4
- data/dbf.gemspec +4 -4
- data/lib/dbf/column/base.rb +2 -6
- data/lib/dbf/memo/base.rb +10 -13
- data/lib/dbf/memo/dbase3.rb +2 -2
- data/lib/dbf/record.rb +19 -21
- data/lib/dbf/table.rb +27 -23
- data/lib/dbf/version.rb +1 -1
- data/spec/dbf/column_spec.rb +89 -79
- data/spec/dbf/file_formats_spec.rb +57 -57
- data/spec/dbf/record_spec.rb +32 -24
- data/spec/dbf/table_spec.rb +49 -49
- data/spec/fixtures/dbase_83_missing_memo.dbf +0 -0
- data/spec/spec_helper.rb +2 -1
- metadata +8 -7
data/lib/dbf/table.rb
CHANGED
@@ -15,15 +15,18 @@ module DBF
|
|
15
15
|
"07" => "Visual Objects 1.x",
|
16
16
|
"30" => "Visual FoxPro",
|
17
17
|
"31" => "Visual FoxPro with AutoIncrement field",
|
18
|
+
"43" => "dBASE IV SQL table files, no memo",
|
19
|
+
"63" => "dBASE IV SQL system files, no memo",
|
18
20
|
"7b" => "dBase IV with memo file",
|
19
21
|
"83" => "dBase III with memo file",
|
20
22
|
"87" => "Visual Objects 1.x with memo file",
|
21
23
|
"8b" => "dBase IV with memo file",
|
22
24
|
"8e" => "dBase IV with SQL table",
|
25
|
+
"cb" => "dBASE IV SQL table files, with memo",
|
23
26
|
"f5" => "FoxPro with memo file",
|
24
27
|
"fb" => "FoxPro without memo file"
|
25
28
|
}
|
26
|
-
|
29
|
+
|
27
30
|
FOXPRO_VERSIONS = {
|
28
31
|
"30" => "Visual FoxPro",
|
29
32
|
"31" => "Visual FoxPro with AutoIncrement field",
|
@@ -58,11 +61,11 @@ module DBF
|
|
58
61
|
# @param [optional String, Encoding] encoding Name of the encoding or an Encoding object
|
59
62
|
def initialize(data, memo = nil, encoding = nil)
|
60
63
|
@data = open_data(data)
|
61
|
-
get_header_info
|
62
|
-
@encoding = encoding
|
64
|
+
@version, @record_count, @header_length, @record_length, @encoding_key, @encoding = get_header_info
|
65
|
+
@encoding = encoding if encoding
|
63
66
|
@memo = open_memo(data, memo)
|
64
67
|
end
|
65
|
-
|
68
|
+
|
66
69
|
# @return [TrueClass, FalseClass]
|
67
70
|
def has_memo_file?
|
68
71
|
!!@memo
|
@@ -75,7 +78,7 @@ module DBF
|
|
75
78
|
@data.close
|
76
79
|
@memo && @memo.close
|
77
80
|
end
|
78
|
-
|
81
|
+
|
79
82
|
# @return [TrueClass, FalseClass]
|
80
83
|
def closed?
|
81
84
|
if @memo
|
@@ -84,7 +87,7 @@ module DBF
|
|
84
87
|
@data.closed?
|
85
88
|
end
|
86
89
|
end
|
87
|
-
|
90
|
+
|
88
91
|
# @return String
|
89
92
|
def filename
|
90
93
|
File.basename @data.path
|
@@ -213,32 +216,28 @@ module DBF
|
|
213
216
|
columns
|
214
217
|
end
|
215
218
|
end
|
216
|
-
|
219
|
+
|
217
220
|
def supports_encoding?
|
218
221
|
String.new.respond_to?(:encoding)
|
219
222
|
end
|
220
|
-
|
223
|
+
|
221
224
|
def supports_iconv?
|
222
225
|
require 'iconv'
|
223
226
|
true
|
224
227
|
rescue
|
225
228
|
false
|
226
229
|
end
|
227
|
-
|
230
|
+
|
228
231
|
def foxpro?
|
229
232
|
FOXPRO_VERSIONS.keys.include? @version
|
230
233
|
end
|
231
234
|
|
232
235
|
private
|
233
|
-
|
236
|
+
|
234
237
|
def column_class #nodoc
|
235
|
-
@column_class ||=
|
236
|
-
Column::Foxpro
|
237
|
-
else
|
238
|
-
Column::Dbase
|
239
|
-
end
|
238
|
+
@column_class ||= foxpro? ? Column::Foxpro : Column::Dbase
|
240
239
|
end
|
241
|
-
|
240
|
+
|
242
241
|
def memo_class #nodoc
|
243
242
|
@memo_class ||= if foxpro?
|
244
243
|
Memo::Foxpro
|
@@ -250,7 +249,7 @@ module DBF
|
|
250
249
|
end
|
251
250
|
end
|
252
251
|
end
|
253
|
-
|
252
|
+
|
254
253
|
def column_count #nodoc
|
255
254
|
@column_count ||= ((@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE).to_i
|
256
255
|
end
|
@@ -265,15 +264,19 @@ module DBF
|
|
265
264
|
elsif memo
|
266
265
|
memo_class.open(memo, version)
|
267
266
|
elsif !data.is_a? StringIO
|
268
|
-
|
269
|
-
basename = File.basename(data, '.*')
|
270
|
-
files = Dir.glob("#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}")
|
267
|
+
files = Dir.glob(memo_search_path(data))
|
271
268
|
files.any? ? memo_class.open(files.first, version) : nil
|
272
269
|
else
|
273
270
|
nil
|
274
271
|
end
|
275
272
|
end
|
276
273
|
|
274
|
+
def memo_search_path(io) #nodoc
|
275
|
+
dirname = File.dirname(io)
|
276
|
+
basename = File.basename(io, '.*')
|
277
|
+
"#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}"
|
278
|
+
end
|
279
|
+
|
277
280
|
def find_all(options) #nodoc
|
278
281
|
map do |record|
|
279
282
|
if record && record.match?(options)
|
@@ -293,10 +296,11 @@ module DBF
|
|
293
296
|
|
294
297
|
def get_header_info #nodoc
|
295
298
|
@data.rewind
|
296
|
-
|
297
|
-
|
299
|
+
version, record_count, header_length, record_length, encoding_key = read_header
|
300
|
+
encoding = ENCODINGS[encoding_key] if supports_encoding? || supports_iconv?
|
301
|
+
[version, record_count, header_length, record_length, encoding_key, encoding]
|
298
302
|
end
|
299
|
-
|
303
|
+
|
300
304
|
def read_header #nodoc
|
301
305
|
@data.read(DBF_HEADER_SIZE).unpack("H2 x3 V v2 x17H2")
|
302
306
|
end
|
data/lib/dbf/version.rb
CHANGED
data/spec/dbf/column_spec.rb
CHANGED
@@ -1,235 +1,245 @@
|
|
1
|
+
# encoding: ascii-8bit
|
2
|
+
|
1
3
|
require "spec_helper"
|
2
4
|
|
3
5
|
describe DBF::Column::Dbase do
|
4
|
-
|
6
|
+
|
5
7
|
context "when initialized" do
|
6
8
|
let(:column) { DBF::Column::Dbase.new "ColumnName", "N", 1, 0, "30" }
|
7
|
-
|
8
|
-
it "sets
|
9
|
-
column.name.
|
9
|
+
|
10
|
+
it "sets :name accessor" do
|
11
|
+
expect(column.name).to eq "ColumnName"
|
10
12
|
end
|
11
|
-
|
12
|
-
it "sets
|
13
|
-
column.type.
|
13
|
+
|
14
|
+
it "sets :type accessor" do
|
15
|
+
expect(column.type).to eq "N"
|
14
16
|
end
|
15
|
-
|
17
|
+
|
16
18
|
it "sets the #length accessor" do
|
17
|
-
column.length.
|
19
|
+
expect(column.length).to eq 1
|
18
20
|
end
|
19
|
-
|
21
|
+
|
20
22
|
it "sets the #decimal accessor" do
|
21
|
-
column.decimal.
|
23
|
+
expect(column.decimal).to eq 0
|
22
24
|
end
|
23
|
-
|
25
|
+
|
24
26
|
describe 'with length of 0' do
|
25
27
|
it 'raises DBF::Column::LengthError' do
|
26
28
|
expect { DBF::Column::Dbase.new "ColumnName", "N", 0, 0, "30" }.to raise_error(DBF::Column::LengthError)
|
27
29
|
end
|
28
30
|
end
|
29
|
-
|
31
|
+
|
30
32
|
describe 'with length less than 0' do
|
31
33
|
it 'raises DBF::Column::LengthError' do
|
32
34
|
expect { DBF::Column::Dbase.new "ColumnName", "N", -1, 0, "30" }.to raise_error(DBF::Column::LengthError)
|
33
35
|
end
|
34
36
|
end
|
35
|
-
|
37
|
+
|
36
38
|
describe 'with empty column name' do
|
37
39
|
it 'raises DBF::Column::NameError' do
|
38
40
|
expect { DBF::Column::Dbase.new "\xFF\xFC", "N", 1, 0, "30" }.to raise_error(DBF::Column::NameError)
|
39
41
|
end
|
40
42
|
end
|
41
43
|
end
|
42
|
-
|
44
|
+
|
43
45
|
context '#type_cast' do
|
44
46
|
context 'with type N (number)' do
|
45
47
|
context 'and 0 decimals' do
|
46
48
|
it 'casts value to Fixnum' do
|
47
49
|
value = '135'
|
48
50
|
column = DBF::Column::Dbase.new "ColumnName", "N", 3, 0, "30"
|
49
|
-
column.type_cast(value).
|
50
|
-
column.type_cast(value).
|
51
|
+
expect(column.type_cast(value)).to be_a(Fixnum)
|
52
|
+
expect(column.type_cast(value)).to eq 135
|
51
53
|
end
|
52
54
|
end
|
53
|
-
|
55
|
+
|
54
56
|
context 'and more than 0 decimals' do
|
55
57
|
it 'casts value to Float' do
|
56
58
|
value = '13.5'
|
57
59
|
column = DBF::Column::Dbase.new "ColumnName", "N", 2, 1, "30"
|
58
|
-
column.type_cast(value).
|
59
|
-
column.type_cast(value).
|
60
|
+
expect(column.type_cast(value)).to be_a(Float)
|
61
|
+
expect(column.type_cast(value)).to eq 13.5
|
60
62
|
end
|
61
63
|
end
|
62
64
|
end
|
63
|
-
|
65
|
+
|
64
66
|
context 'with type F (float)' do
|
65
67
|
it 'casts value to Float' do
|
66
68
|
value = '135'
|
67
69
|
column = DBF::Column::Dbase.new "ColumnName", "F", 3, 0, "30"
|
68
|
-
column.type_cast(value).
|
69
|
-
column.type_cast(value).
|
70
|
+
expect(column.type_cast(value)).to be_a(Float)
|
71
|
+
expect(column.type_cast(value)).to eq 135.0
|
70
72
|
end
|
71
73
|
end
|
72
|
-
|
74
|
+
|
73
75
|
context 'with type I (integer)' do
|
74
76
|
it "casts value to Fixnum" do
|
75
77
|
value = "\203\171\001\000"
|
76
78
|
column = DBF::Column::Dbase.new "ColumnName", "I", 3, 0, "30"
|
77
|
-
column.type_cast(value).
|
79
|
+
expect(column.type_cast(value)).to eq 96643
|
78
80
|
end
|
79
81
|
end
|
80
|
-
|
82
|
+
|
81
83
|
context 'with type L (logical/boolean)' do
|
82
84
|
let(:column) { DBF::Column::Dbase.new "ColumnName", "L", 1, 0, "30" }
|
83
|
-
|
85
|
+
|
84
86
|
it "casts 'y' to true" do
|
85
|
-
column.type_cast('y').
|
87
|
+
expect(column.type_cast('y')).to eq true
|
86
88
|
end
|
87
|
-
|
89
|
+
|
88
90
|
it "casts 't' to true" do
|
89
|
-
column.type_cast('t').
|
91
|
+
expect(column.type_cast('t')).to eq true
|
90
92
|
end
|
91
|
-
|
93
|
+
|
92
94
|
it "casts value other than 't' or 'y' to false" do
|
93
|
-
column.type_cast('n').
|
95
|
+
expect(column.type_cast('n')).to eq false
|
94
96
|
end
|
95
97
|
end
|
96
|
-
|
98
|
+
|
97
99
|
context 'with type T (datetime)' do
|
98
100
|
let(:column) { DBF::Column::Dbase.new "ColumnName", "T", 16, 0, "30" }
|
99
|
-
|
101
|
+
|
100
102
|
context 'with valid datetime' do
|
101
103
|
it "casts to DateTime" do
|
102
|
-
column.type_cast("Nl%\000\300Z\252\003").
|
104
|
+
expect(column.type_cast("Nl%\000\300Z\252\003")).to eq DateTime.parse("2002-10-10T17:04:56+00:00")
|
103
105
|
end
|
104
106
|
end
|
105
|
-
|
107
|
+
|
106
108
|
if ruby_supports_mathn?
|
107
109
|
context 'when requiring mathn' do
|
108
110
|
it "casts to DateTime" do
|
109
|
-
lambda
|
111
|
+
with_mathn = lambda do
|
110
112
|
require 'mathn'
|
111
113
|
column.type_cast("Nl%\000\300Z\252\003")
|
112
|
-
|
114
|
+
end
|
115
|
+
expect(with_mathn.call).to eq DateTime.parse("2002-10-10T17:04:56+00:00")
|
113
116
|
end
|
114
117
|
end
|
115
118
|
end
|
116
|
-
|
119
|
+
|
117
120
|
context 'with invalid datetime' do
|
118
121
|
it "casts to nil" do
|
119
|
-
column.type_cast("Nl%\000\000A\000\999").
|
122
|
+
expect(column.type_cast("Nl%\000\000A\000\999")).to be_nil
|
120
123
|
end
|
121
124
|
end
|
122
125
|
end
|
123
|
-
|
126
|
+
|
124
127
|
context 'with type D (date)' do
|
125
128
|
let(:column) { DBF::Column::Dbase.new "ColumnName", "D", 8, 0, "30" }
|
126
|
-
|
129
|
+
|
127
130
|
context 'with valid date' do
|
128
131
|
it "casts to Date" do
|
129
|
-
column.type_cast("20050712").
|
132
|
+
expect(column.type_cast("20050712")).to eq Date.new(2005,7,12)
|
130
133
|
end
|
131
134
|
end
|
132
|
-
|
135
|
+
|
133
136
|
context 'with invalid date' do
|
134
137
|
it "casts to nil" do
|
135
|
-
column.type_cast("0").
|
138
|
+
expect(column.type_cast("0")).to be_nil
|
136
139
|
end
|
137
140
|
end
|
138
141
|
end
|
139
|
-
|
142
|
+
|
140
143
|
context 'with type M (memo)' do
|
141
144
|
it "casts to string" do
|
142
145
|
column = DBF::Column::Dbase.new "ColumnName", "M", 3, 0, "30"
|
143
|
-
column.type_cast('abc').
|
146
|
+
expect(column.type_cast('abc')).to be_a(String)
|
147
|
+
end
|
148
|
+
|
149
|
+
it 'casts nil to nil' do
|
150
|
+
column = DBF::Column::Dbase.new "ColumnName", "M", 3, 0, "30"
|
151
|
+
expect(column.type_cast(nil)).to be_nil
|
144
152
|
end
|
145
153
|
end
|
146
154
|
end
|
147
|
-
|
155
|
+
|
148
156
|
context 'with type Y (currency)' do
|
149
157
|
let(:column) { DBF::Column::Dbase.new "ColumnName", "Y", 8, 4, "31" }
|
150
|
-
|
158
|
+
|
151
159
|
it 'casts to float' do
|
152
|
-
column.type_cast(" \xBF\x02\x00\x00\x00\x00\x00").
|
160
|
+
expect(column.type_cast(" \xBF\x02\x00\x00\x00\x00\x00")).to eq 18.0
|
153
161
|
end
|
154
162
|
end
|
155
|
-
|
163
|
+
|
156
164
|
context "#schema_definition" do
|
157
165
|
context 'with type N (number)' do
|
158
166
|
it "outputs an integer column" do
|
159
167
|
column = DBF::Column::Dbase.new "ColumnName", "N", 1, 0, "30"
|
160
|
-
column.schema_definition.
|
168
|
+
expect(column.schema_definition).to eq "\"column_name\", :integer\n"
|
161
169
|
end
|
162
170
|
end
|
163
|
-
|
171
|
+
|
164
172
|
context "with type B (binary)" do
|
165
173
|
context "with Foxpro dbf" do
|
166
174
|
context "when decimal is greater than 0" do
|
167
175
|
it "outputs an float column" do
|
168
176
|
column = DBF::Column::Dbase.new "ColumnName", "B", 1, 2, "f5"
|
169
|
-
column.schema_definition.
|
177
|
+
expect(column.schema_definition).to eq "\"column_name\", :float\n"
|
170
178
|
end
|
171
179
|
end
|
172
|
-
|
180
|
+
|
173
181
|
context "when decimal is 0" do
|
174
|
-
|
175
|
-
|
182
|
+
it "outputs an integer column" do
|
183
|
+
column = DBF::Column::Dbase.new "ColumnName", "B", 1, 0, "f5"
|
184
|
+
expect(column.schema_definition).to eq "\"column_name\", :integer\n"
|
185
|
+
end
|
176
186
|
end
|
177
187
|
end
|
178
188
|
end
|
179
|
-
|
189
|
+
|
180
190
|
it "defines a float colmn if type is (N)umber with more than 0 decimals" do
|
181
191
|
column = DBF::Column::Dbase.new "ColumnName", "N", 1, 2, "30"
|
182
|
-
column.schema_definition.
|
192
|
+
expect(column.schema_definition).to eq "\"column_name\", :float\n"
|
183
193
|
end
|
184
|
-
|
194
|
+
|
185
195
|
it "defines a date column if type is (D)ate" do
|
186
196
|
column = DBF::Column::Dbase.new "ColumnName", "D", 8, 0, "30"
|
187
|
-
column.schema_definition.
|
197
|
+
expect(column.schema_definition).to eq "\"column_name\", :date\n"
|
188
198
|
end
|
189
|
-
|
199
|
+
|
190
200
|
it "defines a datetime column if type is (D)ate" do
|
191
201
|
column = DBF::Column::Dbase.new "ColumnName", "T", 16, 0, "30"
|
192
|
-
column.schema_definition.
|
202
|
+
expect(column.schema_definition).to eq "\"column_name\", :datetime\n"
|
193
203
|
end
|
194
|
-
|
204
|
+
|
195
205
|
it "defines a boolean column if type is (L)ogical" do
|
196
206
|
column = DBF::Column::Dbase.new "ColumnName", "L", 1, 0, "30"
|
197
|
-
column.schema_definition.
|
207
|
+
expect(column.schema_definition).to eq "\"column_name\", :boolean\n"
|
198
208
|
end
|
199
|
-
|
209
|
+
|
200
210
|
it "defines a text column if type is (M)emo" do
|
201
211
|
column = DBF::Column::Dbase.new "ColumnName", "M", 1, 0, "30"
|
202
|
-
column.schema_definition.
|
212
|
+
expect(column.schema_definition).to eq "\"column_name\", :text\n"
|
203
213
|
end
|
204
|
-
|
214
|
+
|
205
215
|
it "defines a string column with length for any other data types" do
|
206
216
|
column = DBF::Column::Dbase.new "ColumnName", "X", 20, 0, "30"
|
207
|
-
column.schema_definition.
|
217
|
+
expect(column.schema_definition).to eq "\"column_name\", :string, :limit => 20\n"
|
208
218
|
end
|
209
219
|
end
|
210
|
-
|
211
|
-
context "#name" do
|
220
|
+
|
221
|
+
context "#name" do
|
212
222
|
it "contains only ASCII characters" do
|
213
223
|
column = DBF::Column::Dbase.new "--\x1F-\x68\x65\x6C\x6C\x6F world-\x80--", "N", 1, 0, "30"
|
214
|
-
column.name.
|
224
|
+
expect(column.name).to eq "---hello world---"
|
215
225
|
end
|
216
226
|
|
217
227
|
it "is truncated at the null character" do
|
218
228
|
column = DBF::Column::Dbase.new "--\x1F-\x68\x65\x6C\x6C\x6F \x00 world-\x80--", "N", 1, 0, "30"
|
219
|
-
column.name.
|
229
|
+
expect(column.name).to eq "---hello "
|
220
230
|
end
|
221
231
|
end
|
222
|
-
|
232
|
+
|
223
233
|
context '#decode_date' do
|
224
234
|
let(:column) { DBF::Column::Dbase.new "ColumnName", "N", 1, 0, "30" }
|
225
|
-
|
235
|
+
|
226
236
|
it 'is nil if value is blank' do
|
227
|
-
column.send(:decode_date, '').
|
237
|
+
expect(column.send(:decode_date, '')).to be_nil
|
228
238
|
end
|
229
|
-
|
239
|
+
|
230
240
|
it 'interperets spaces as zeros' do
|
231
|
-
column.send(:decode_date, '2010 715').
|
241
|
+
expect(column.send(:decode_date, '2010 715')).to eq Date.parse('20100715')
|
232
242
|
end
|
233
243
|
end
|
234
|
-
|
244
|
+
|
235
245
|
end
|