dbf 5.1.0 → 5.2.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +4 -2
- data/dbf.gemspec +6 -9
- data/lib/dbf/column.rb +19 -15
- data/lib/dbf/column_builder.rb +31 -0
- data/lib/dbf/column_type.rb +61 -15
- data/lib/dbf/database/foxpro.rb +21 -32
- data/lib/dbf/encodings.rb +2 -0
- data/lib/dbf/file_handler.rb +36 -0
- data/lib/dbf/find.rb +54 -0
- data/lib/dbf/header.rb +7 -8
- data/lib/dbf/memo/base.rb +4 -0
- data/lib/dbf/memo/dbase3.rb +5 -3
- data/lib/dbf/memo/dbase4.rb +4 -2
- data/lib/dbf/memo/foxpro.rb +16 -7
- data/lib/dbf/record.rb +62 -34
- data/lib/dbf/record_context.rb +5 -0
- data/lib/dbf/record_iterator.rb +35 -0
- data/lib/dbf/schema.rb +23 -21
- data/lib/dbf/table.rb +44 -178
- data/lib/dbf/version.rb +3 -1
- data/lib/dbf/version_config.rb +79 -0
- data/lib/dbf.rb +8 -0
- metadata +15 -64
- data/spec/dbf/column_spec.rb +0 -286
- data/spec/dbf/database/foxpro_spec.rb +0 -51
- data/spec/dbf/encoding_spec.rb +0 -47
- data/spec/dbf/file_formats_spec.rb +0 -219
- data/spec/dbf/record_spec.rb +0 -114
- data/spec/dbf/table_spec.rb +0 -375
- data/spec/fixtures/cp1251.dbf +0 -0
- data/spec/fixtures/cp1251_summary.txt +0 -12
- data/spec/fixtures/dbase_02.dbf +0 -0
- data/spec/fixtures/dbase_02_summary.txt +0 -23
- data/spec/fixtures/dbase_03.dbf +0 -0
- data/spec/fixtures/dbase_03_cyrillic.dbf +0 -0
- data/spec/fixtures/dbase_03_cyrillic_summary.txt +0 -11
- data/spec/fixtures/dbase_03_summary.txt +0 -40
- data/spec/fixtures/dbase_30.dbf +0 -0
- data/spec/fixtures/dbase_30.fpt +0 -0
- data/spec/fixtures/dbase_30_summary.txt +0 -154
- data/spec/fixtures/dbase_31.dbf +0 -0
- data/spec/fixtures/dbase_31_summary.txt +0 -20
- data/spec/fixtures/dbase_32.dbf +0 -0
- data/spec/fixtures/dbase_32_summary.txt +0 -11
- data/spec/fixtures/dbase_83.dbf +0 -0
- data/spec/fixtures/dbase_83.dbt +0 -0
- data/spec/fixtures/dbase_83_missing_memo.dbf +0 -0
- data/spec/fixtures/dbase_83_missing_memo_record_0.yml +0 -16
- data/spec/fixtures/dbase_83_record_0.yml +0 -16
- data/spec/fixtures/dbase_83_record_9.yml +0 -23
- data/spec/fixtures/dbase_83_schema_ar.txt +0 -19
- data/spec/fixtures/dbase_83_schema_sq.txt +0 -21
- data/spec/fixtures/dbase_83_schema_sq_lim.txt +0 -21
- data/spec/fixtures/dbase_83_summary.txt +0 -24
- data/spec/fixtures/dbase_8b.dbf +0 -0
- data/spec/fixtures/dbase_8b.dbt +0 -0
- data/spec/fixtures/dbase_8b_summary.txt +0 -15
- data/spec/fixtures/dbase_8c.dbf +0 -0
- data/spec/fixtures/dbase_f5.dbf +0 -0
- data/spec/fixtures/dbase_f5.fpt +0 -0
- data/spec/fixtures/dbase_f5_summary.txt +0 -68
- data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DBC +0 -0
- data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DCT +0 -0
- data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DCX +0 -0
- data/spec/fixtures/foxprodb/calls.CDX +0 -0
- data/spec/fixtures/foxprodb/calls.FPT +0 -0
- data/spec/fixtures/foxprodb/calls.dbf +0 -0
- data/spec/fixtures/foxprodb/contacts.CDX +0 -0
- data/spec/fixtures/foxprodb/contacts.FPT +0 -0
- data/spec/fixtures/foxprodb/contacts.dbf +0 -0
- data/spec/fixtures/foxprodb/setup.CDX +0 -0
- data/spec/fixtures/foxprodb/setup.dbf +0 -0
- data/spec/fixtures/foxprodb/types.CDX +0 -0
- data/spec/fixtures/foxprodb/types.dbf +0 -0
- data/spec/fixtures/polygon.dbf +0 -0
- data/spec/spec_helper.rb +0 -33
data/lib/dbf/record.rb
CHANGED
|
@@ -1,17 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module DBF
|
|
2
4
|
# An instance of DBF::Record represents a row in the DBF file
|
|
3
5
|
class Record
|
|
4
6
|
# Initialize a new DBF::Record
|
|
5
7
|
#
|
|
6
8
|
# @param data [String, StringIO] data
|
|
7
|
-
# @param
|
|
8
|
-
# @param
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
@
|
|
12
|
-
@
|
|
13
|
-
@
|
|
14
|
-
@memo = memo
|
|
9
|
+
# @param context [DBF::RecordContext]
|
|
10
|
+
# @param offset [Integer]
|
|
11
|
+
def initialize(data, context, offset = 0)
|
|
12
|
+
@data = data
|
|
13
|
+
@context = context
|
|
14
|
+
@offset = offset
|
|
15
|
+
@to_a = nil
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
# Equality
|
|
@@ -19,7 +20,9 @@ module DBF
|
|
|
19
20
|
# @param [DBF::Record] other
|
|
20
21
|
# @return [Boolean]
|
|
21
22
|
def ==(other)
|
|
22
|
-
|
|
23
|
+
attributes == other.attributes
|
|
24
|
+
rescue NoMethodError
|
|
25
|
+
false
|
|
23
26
|
end
|
|
24
27
|
|
|
25
28
|
# Reads attributes by column name
|
|
@@ -27,10 +30,13 @@ module DBF
|
|
|
27
30
|
# @param name [String, Symbol] key
|
|
28
31
|
def [](name)
|
|
29
32
|
key = name.to_s
|
|
30
|
-
if
|
|
33
|
+
if @context.column_offsets && !@to_a
|
|
34
|
+
index = column_name_index(key)
|
|
35
|
+
index ? column_value(index) : nil
|
|
36
|
+
elsif attributes.key?(key)
|
|
31
37
|
attributes[key]
|
|
32
38
|
elsif (index = underscored_column_names.index(key))
|
|
33
|
-
attributes[@columns[index].name]
|
|
39
|
+
attributes[@context.columns[index].name]
|
|
34
40
|
end
|
|
35
41
|
end
|
|
36
42
|
|
|
@@ -53,43 +59,65 @@ module DBF
|
|
|
53
59
|
#
|
|
54
60
|
# @return [Array]
|
|
55
61
|
def to_a
|
|
56
|
-
@to_a ||=
|
|
62
|
+
@to_a ||= begin
|
|
63
|
+
data = @data
|
|
64
|
+
offset = @offset
|
|
65
|
+
columns = @context.columns
|
|
66
|
+
col_count = columns.length
|
|
67
|
+
result = Array.new(col_count)
|
|
68
|
+
index = 0
|
|
69
|
+
while index < col_count
|
|
70
|
+
column = columns[index]
|
|
71
|
+
len = column.length
|
|
72
|
+
raw = data.byteslice(offset, len)
|
|
73
|
+
offset += len
|
|
74
|
+
result[index] = decode_column(raw, column)
|
|
75
|
+
index += 1
|
|
76
|
+
end
|
|
77
|
+
@offset = offset
|
|
78
|
+
result
|
|
79
|
+
end
|
|
57
80
|
end
|
|
58
81
|
|
|
59
82
|
private
|
|
60
83
|
|
|
61
|
-
def
|
|
62
|
-
|
|
84
|
+
def decode_memo_value(raw) # :nodoc:
|
|
85
|
+
memo = @context.memo
|
|
86
|
+
return nil unless memo
|
|
87
|
+
|
|
88
|
+
version = @context.version
|
|
89
|
+
raw = raw.unpack1('V') if version == '30' || version == '31'
|
|
90
|
+
memo.get(raw.to_i)
|
|
63
91
|
end
|
|
64
92
|
|
|
65
|
-
def
|
|
66
|
-
|
|
93
|
+
def column_name_index(key) # :nodoc:
|
|
94
|
+
column_names.index(key) || underscored_column_names.index(key)
|
|
67
95
|
end
|
|
68
96
|
|
|
69
|
-
def
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
nil
|
|
76
|
-
end
|
|
97
|
+
def column_value(index) # :nodoc:
|
|
98
|
+
column = @context.columns[index]
|
|
99
|
+
col_offset = @offset + @context.column_offsets[index]
|
|
100
|
+
len = column.length
|
|
101
|
+
raw = @data.byteslice(col_offset, len)
|
|
102
|
+
decode_column(raw, column)
|
|
77
103
|
end
|
|
78
104
|
|
|
79
|
-
def
|
|
80
|
-
|
|
81
|
-
column.type_cast(value)
|
|
105
|
+
def decode_column(raw, column) # :nodoc:
|
|
106
|
+
column.decode(raw) { |raw_memo| decode_memo_value(raw_memo) }
|
|
82
107
|
end
|
|
83
108
|
|
|
84
|
-
def
|
|
85
|
-
|
|
86
|
-
data = data.unpack1('V') if %w[30 31].include?(@version)
|
|
87
|
-
data.to_i
|
|
109
|
+
def column_names # :nodoc:
|
|
110
|
+
@column_names ||= @context.columns.map(&:name)
|
|
88
111
|
end
|
|
89
112
|
|
|
90
113
|
def method_missing(method, *args) # :nodoc:
|
|
91
|
-
|
|
92
|
-
|
|
114
|
+
key = method.to_s
|
|
115
|
+
if (index = underscored_column_names.index(key))
|
|
116
|
+
if @context.column_offsets && !@to_a
|
|
117
|
+
column_value(index)
|
|
118
|
+
else
|
|
119
|
+
attributes[@context.columns[index].name]
|
|
120
|
+
end
|
|
93
121
|
else
|
|
94
122
|
super
|
|
95
123
|
end
|
|
@@ -100,7 +128,7 @@ module DBF
|
|
|
100
128
|
end
|
|
101
129
|
|
|
102
130
|
def underscored_column_names # :nodoc:
|
|
103
|
-
@underscored_column_names ||= @columns.map(&:underscored_name)
|
|
131
|
+
@underscored_column_names ||= @context.columns.map(&:underscored_name)
|
|
104
132
|
end
|
|
105
133
|
end
|
|
106
134
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBF
|
|
4
|
+
class RecordIterator
|
|
5
|
+
def initialize(data, context, header_length, record_length, record_count)
|
|
6
|
+
@data = data
|
|
7
|
+
@context = context
|
|
8
|
+
@header_length = header_length
|
|
9
|
+
@record_length = record_length
|
|
10
|
+
@record_count = record_count
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def each
|
|
14
|
+
buf = read_buffer
|
|
15
|
+
return unless buf
|
|
16
|
+
|
|
17
|
+
pos = 0
|
|
18
|
+
@record_count.times do
|
|
19
|
+
if buf.getbyte(pos) == 0x2A
|
|
20
|
+
yield nil
|
|
21
|
+
else
|
|
22
|
+
yield Record.new(buf, @context, pos + 1)
|
|
23
|
+
end
|
|
24
|
+
pos += @record_length
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def read_buffer
|
|
31
|
+
@data.seek(@header_length)
|
|
32
|
+
@data.read(@record_length * @record_count)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/dbf/schema.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module DBF
|
|
2
4
|
# The Schema module is mixin for the Table class
|
|
3
5
|
module Schema
|
|
@@ -12,6 +14,11 @@ module DBF
|
|
|
12
14
|
'B' => ':binary'
|
|
13
15
|
}.freeze
|
|
14
16
|
|
|
17
|
+
STRING_DATA_FORMATS = {
|
|
18
|
+
sequel: ':varchar, :size => %s',
|
|
19
|
+
activerecord: ':string, :limit => %s'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
15
22
|
# Generate an ActiveRecord::Schema
|
|
16
23
|
#
|
|
17
24
|
# xBase data types are converted to generic types as follows:
|
|
@@ -47,27 +54,25 @@ module DBF
|
|
|
47
54
|
end
|
|
48
55
|
|
|
49
56
|
def activerecord_schema(*) # :nodoc:
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
output = +"ActiveRecord::Schema.define do\n"
|
|
58
|
+
output << " create_table \"#{name}\" do |t|\n"
|
|
52
59
|
columns.each do |column|
|
|
53
|
-
|
|
60
|
+
output << " t.column #{activerecord_schema_definition(column)}"
|
|
54
61
|
end
|
|
55
|
-
|
|
56
|
-
|
|
62
|
+
output << " end\nend"
|
|
63
|
+
output
|
|
57
64
|
end
|
|
58
65
|
|
|
59
66
|
def sequel_schema(table_only: false) # :nodoc:
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
s << " create_table(:#{name}) do\n"
|
|
67
|
+
output = +''
|
|
68
|
+
output << "Sequel.migration do\n change do\n " unless table_only
|
|
69
|
+
output << " create_table(:#{name}) do\n"
|
|
64
70
|
columns.each do |column|
|
|
65
|
-
|
|
71
|
+
output << " column #{sequel_schema_definition(column)}"
|
|
66
72
|
end
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
s
|
|
73
|
+
output << " end\n"
|
|
74
|
+
output << " end\nend\n" unless table_only
|
|
75
|
+
output
|
|
71
76
|
end
|
|
72
77
|
|
|
73
78
|
def json_schema(*) # :nodoc:
|
|
@@ -91,11 +96,12 @@ module DBF
|
|
|
91
96
|
end
|
|
92
97
|
|
|
93
98
|
def schema_data_type(column, format = :activerecord) # :nodoc:
|
|
94
|
-
|
|
99
|
+
col_type = column.type
|
|
100
|
+
case col_type
|
|
95
101
|
when 'N', 'F', 'I'
|
|
96
102
|
number_data_type(column)
|
|
97
103
|
when 'Y', 'D', 'T', 'L', 'M', 'B'
|
|
98
|
-
OTHER_DATA_TYPES[
|
|
104
|
+
OTHER_DATA_TYPES[col_type]
|
|
99
105
|
else
|
|
100
106
|
string_data_format(format, column)
|
|
101
107
|
end
|
|
@@ -106,11 +112,7 @@ module DBF
|
|
|
106
112
|
end
|
|
107
113
|
|
|
108
114
|
def string_data_format(format, column)
|
|
109
|
-
|
|
110
|
-
":varchar, :size => #{column.length}"
|
|
111
|
-
else
|
|
112
|
-
":string, :limit => #{column.length}"
|
|
113
|
-
end
|
|
115
|
+
STRING_DATA_FORMATS.fetch(format, STRING_DATA_FORMATS[:activerecord]) % column.length
|
|
114
116
|
end
|
|
115
117
|
end
|
|
116
118
|
end
|
data/lib/dbf/table.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
module DBF
|
|
2
4
|
class FileNotFoundError < StandardError
|
|
3
5
|
end
|
|
@@ -11,42 +13,9 @@ module DBF
|
|
|
11
13
|
extend Forwardable
|
|
12
14
|
include Enumerable
|
|
13
15
|
include ::DBF::Schema
|
|
16
|
+
include ::DBF::Find
|
|
14
17
|
|
|
15
|
-
|
|
16
|
-
DBASE3_HEADER_SIZE = 32
|
|
17
|
-
DBASE7_HEADER_SIZE = 68
|
|
18
|
-
|
|
19
|
-
VERSIONS = {
|
|
20
|
-
'02' => 'FoxBase',
|
|
21
|
-
'03' => 'dBase III without memo file',
|
|
22
|
-
'04' => 'dBase IV without memo file',
|
|
23
|
-
'05' => 'dBase V without memo file',
|
|
24
|
-
'07' => 'Visual Objects 1.x',
|
|
25
|
-
'30' => 'Visual FoxPro',
|
|
26
|
-
'32' => 'Visual FoxPro with field type Varchar or Varbinary',
|
|
27
|
-
'31' => 'Visual FoxPro with AutoIncrement field',
|
|
28
|
-
'43' => 'dBASE IV SQL table files, no memo',
|
|
29
|
-
'63' => 'dBASE IV SQL system files, no memo',
|
|
30
|
-
'7b' => 'dBase IV with memo file',
|
|
31
|
-
'83' => 'dBase III with memo file',
|
|
32
|
-
'87' => 'Visual Objects 1.x with memo file',
|
|
33
|
-
'8b' => 'dBase IV with memo file',
|
|
34
|
-
'8c' => 'dBase 7',
|
|
35
|
-
'8e' => 'dBase IV with SQL table',
|
|
36
|
-
'cb' => 'dBASE IV SQL table files, with memo',
|
|
37
|
-
'f5' => 'FoxPro with memo file',
|
|
38
|
-
'fb' => 'FoxPro without memo file'
|
|
39
|
-
}.freeze
|
|
40
|
-
|
|
41
|
-
FOXPRO_VERSIONS = {
|
|
42
|
-
'30' => 'Visual FoxPro',
|
|
43
|
-
'31' => 'Visual FoxPro with AutoIncrement field',
|
|
44
|
-
'f5' => 'FoxPro with memo file',
|
|
45
|
-
'fb' => 'FoxPro without memo file'
|
|
46
|
-
}.freeze
|
|
47
|
-
|
|
48
|
-
attr_accessor :encoding
|
|
49
|
-
attr_writer :name
|
|
18
|
+
attr_reader :encoding
|
|
50
19
|
|
|
51
20
|
def_delegator :header, :header_length
|
|
52
21
|
def_delegator :header, :record_count
|
|
@@ -74,10 +43,12 @@ module DBF
|
|
|
74
43
|
# @param data [String, StringIO] data Path to the dbf file or a StringIO object
|
|
75
44
|
# @param memo [optional String, StringIO] memo Path to the memo file or a StringIO object
|
|
76
45
|
# @param encoding [optional String, Encoding] encoding Name of the encoding or an Encoding object
|
|
77
|
-
def initialize(data, memo = nil, encoding = nil)
|
|
78
|
-
@data = open_data(data)
|
|
79
|
-
@
|
|
80
|
-
@
|
|
46
|
+
def initialize(data, memo = nil, encoding = nil, name: nil)
|
|
47
|
+
@data = FileHandler.open_data(data)
|
|
48
|
+
@user_encoding = encoding
|
|
49
|
+
@encoding = determine_encoding
|
|
50
|
+
@memo = FileHandler.open_memo(data, memo, version_config.memo_class, version)
|
|
51
|
+
@name = name
|
|
81
52
|
yield self if block_given?
|
|
82
53
|
end
|
|
83
54
|
|
|
@@ -91,11 +62,7 @@ module DBF
|
|
|
91
62
|
|
|
92
63
|
# @return [TrueClass, FalseClass]
|
|
93
64
|
def closed?
|
|
94
|
-
|
|
95
|
-
@data.closed? && @memo.closed?
|
|
96
|
-
else
|
|
97
|
-
@data.closed?
|
|
98
|
-
end
|
|
65
|
+
@data.closed? && (!@memo || @memo.closed?)
|
|
99
66
|
end
|
|
100
67
|
|
|
101
68
|
# Column names
|
|
@@ -105,6 +72,20 @@ module DBF
|
|
|
105
72
|
@column_names ||= columns.map(&:name)
|
|
106
73
|
end
|
|
107
74
|
|
|
75
|
+
# Cumulative byte offsets for each column within a record
|
|
76
|
+
#
|
|
77
|
+
# @return [Array<Integer>]
|
|
78
|
+
def column_offsets
|
|
79
|
+
@column_offsets ||= begin
|
|
80
|
+
sum = 0
|
|
81
|
+
columns.map { |col| sum.tap { sum += col.length } }
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def record_context
|
|
86
|
+
@record_context ||= RecordContext.new(columns:, version:, memo: @memo, column_offsets:)
|
|
87
|
+
end
|
|
88
|
+
|
|
108
89
|
# All columns
|
|
109
90
|
#
|
|
110
91
|
# @return [Array]
|
|
@@ -116,53 +97,16 @@ module DBF
|
|
|
116
97
|
# if the record has been marked as deleted.
|
|
117
98
|
#
|
|
118
99
|
# @yield [nil, DBF::Record]
|
|
119
|
-
def each
|
|
120
|
-
|
|
100
|
+
def each(&)
|
|
101
|
+
return enum_for(:each) unless block_given?
|
|
102
|
+
return if columns.empty?
|
|
103
|
+
|
|
104
|
+
RecordIterator.new(@data, record_context, header_length, record_length, record_count).each(&)
|
|
121
105
|
end
|
|
122
106
|
|
|
123
107
|
# @return [String]
|
|
124
108
|
def filename
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
File.basename(@data.path)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
# Find records using a simple ActiveRecord-like syntax.
|
|
131
|
-
#
|
|
132
|
-
# Examples:
|
|
133
|
-
# table = DBF::Table.new 'mydata.dbf'
|
|
134
|
-
#
|
|
135
|
-
# # Find record number 5
|
|
136
|
-
# table.find(5)
|
|
137
|
-
#
|
|
138
|
-
# # Find all records for Keith Morrison
|
|
139
|
-
# table.find :all, first_name: "Keith", last_name: "Morrison"
|
|
140
|
-
#
|
|
141
|
-
# # Find first record
|
|
142
|
-
# table.find :first, first_name: "Keith"
|
|
143
|
-
#
|
|
144
|
-
# The <b>command</b> may be a record index, :all, or :first.
|
|
145
|
-
# <b>options</b> is optional and, if specified, should be a hash where the
|
|
146
|
-
# keys correspond to column names in the database. The values will be
|
|
147
|
-
# matched exactly with the value in the database. If you specify more
|
|
148
|
-
# than one key, all values must match in order for the record to be
|
|
149
|
-
# returned. The equivalent SQL would be "WHERE key1 = 'value1'
|
|
150
|
-
# AND key2 = 'value2'".
|
|
151
|
-
#
|
|
152
|
-
# @param command [Integer, Symbol] command
|
|
153
|
-
# @param options [optional, Hash] options Hash of search parameters
|
|
154
|
-
# @yield [optional, DBF::Record, NilClass]
|
|
155
|
-
def find(command, options = {}, &block)
|
|
156
|
-
case command
|
|
157
|
-
when Integer
|
|
158
|
-
record(command)
|
|
159
|
-
when Array
|
|
160
|
-
command.map { |i| record(i) }
|
|
161
|
-
when :all
|
|
162
|
-
find_all(options, &block)
|
|
163
|
-
when :first
|
|
164
|
-
find_first(options)
|
|
165
|
-
end
|
|
109
|
+
File.basename(@data.path) if @data.is_a?(File)
|
|
166
110
|
end
|
|
167
111
|
|
|
168
112
|
# @return [TrueClass, FalseClass]
|
|
@@ -188,7 +132,7 @@ module DBF
|
|
|
188
132
|
return nil if deleted_record?
|
|
189
133
|
|
|
190
134
|
record_data = @data.read(record_length)
|
|
191
|
-
DBF::Record.new(record_data,
|
|
135
|
+
DBF::Record.new(record_data, record_context)
|
|
192
136
|
end
|
|
193
137
|
|
|
194
138
|
alias row record
|
|
@@ -198,8 +142,7 @@ module DBF
|
|
|
198
142
|
#
|
|
199
143
|
# @param [optional String] path Defaults to STDOUT
|
|
200
144
|
def to_csv(path = nil)
|
|
201
|
-
|
|
202
|
-
csv = CSV.new(out_io, force_quotes: true)
|
|
145
|
+
csv = CSV.new(path ? File.open(path, 'w') : $stdout, force_quotes: true)
|
|
203
146
|
csv << column_names
|
|
204
147
|
each { |record| csv << record.to_a }
|
|
205
148
|
end
|
|
@@ -208,7 +151,7 @@ module DBF
|
|
|
208
151
|
#
|
|
209
152
|
# @return [String]
|
|
210
153
|
def version_description
|
|
211
|
-
|
|
154
|
+
version_config.version_description
|
|
212
155
|
end
|
|
213
156
|
|
|
214
157
|
# Encode string
|
|
@@ -228,104 +171,27 @@ module DBF
|
|
|
228
171
|
|
|
229
172
|
private
|
|
230
173
|
|
|
231
|
-
def
|
|
232
|
-
|
|
233
|
-
@data.seek(header_size)
|
|
234
|
-
[].tap do |columns|
|
|
235
|
-
until end_of_record?
|
|
236
|
-
args = case version
|
|
237
|
-
when '02'
|
|
238
|
-
[self, *@data.read(header_size * 2).unpack('A11 a C'), 0]
|
|
239
|
-
when '04', '8c'
|
|
240
|
-
[self, *@data.read(48).unpack('A32 a C C x13')]
|
|
241
|
-
else
|
|
242
|
-
[self, *@data.read(header_size).unpack('A11 a x4 C2')]
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
columns << Column.new(*args)
|
|
246
|
-
end
|
|
247
|
-
end
|
|
248
|
-
end
|
|
174
|
+
def version_config
|
|
175
|
+
@version_config ||= VersionConfig.new(version)
|
|
249
176
|
end
|
|
250
177
|
|
|
251
|
-
def
|
|
252
|
-
|
|
253
|
-
when '02'
|
|
254
|
-
DBASE2_HEADER_SIZE
|
|
255
|
-
when '04', '8c'
|
|
256
|
-
DBASE7_HEADER_SIZE
|
|
257
|
-
else
|
|
258
|
-
DBASE3_HEADER_SIZE
|
|
259
|
-
end
|
|
178
|
+
def determine_encoding
|
|
179
|
+
@user_encoding || header.encoding || Encoding.default_external
|
|
260
180
|
end
|
|
261
181
|
|
|
262
|
-
def
|
|
263
|
-
|
|
264
|
-
flag ? flag.unpack1('a') == '*' : true
|
|
265
|
-
end
|
|
266
|
-
|
|
267
|
-
def end_of_record? # :nodoc:
|
|
268
|
-
safe_seek { @data.read(1).ord == 13 }
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def find_all(options) # :nodoc:
|
|
272
|
-
select do |record|
|
|
273
|
-
next unless record&.match?(options)
|
|
274
|
-
|
|
275
|
-
yield record if block_given?
|
|
276
|
-
record
|
|
277
|
-
end
|
|
278
|
-
end
|
|
279
|
-
|
|
280
|
-
def find_first(options) # :nodoc:
|
|
281
|
-
detect { |record| record&.match?(options) }
|
|
182
|
+
def build_columns # :nodoc:
|
|
183
|
+
ColumnBuilder.new(self, @data, version_config).build
|
|
282
184
|
end
|
|
283
185
|
|
|
284
|
-
def
|
|
285
|
-
|
|
186
|
+
def deleted_record? # :nodoc:
|
|
187
|
+
flag = @data.read(1)
|
|
188
|
+
flag ? flag.getbyte(0) == 0x2A : true
|
|
286
189
|
end
|
|
287
190
|
|
|
288
191
|
def header # :nodoc:
|
|
289
192
|
@header ||= safe_seek do
|
|
290
193
|
@data.seek(0)
|
|
291
|
-
Header.new(@data.read(DBASE3_HEADER_SIZE))
|
|
292
|
-
end
|
|
293
|
-
end
|
|
294
|
-
|
|
295
|
-
def memo_class # :nodoc:
|
|
296
|
-
@memo_class ||= if foxpro?
|
|
297
|
-
Memo::Foxpro
|
|
298
|
-
else
|
|
299
|
-
version == '83' ? Memo::Dbase3 : Memo::Dbase4
|
|
300
|
-
end
|
|
301
|
-
end
|
|
302
|
-
|
|
303
|
-
def memo_search_path(io) # :nodoc:
|
|
304
|
-
dirname = File.dirname(io)
|
|
305
|
-
basename = File.basename(io, '.*')
|
|
306
|
-
"#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}"
|
|
307
|
-
end
|
|
308
|
-
|
|
309
|
-
def open_data(data) # :nodoc:
|
|
310
|
-
case data
|
|
311
|
-
when StringIO
|
|
312
|
-
data
|
|
313
|
-
when String
|
|
314
|
-
File.open(data, 'rb')
|
|
315
|
-
else
|
|
316
|
-
raise ArgumentError, 'data must be a file path or StringIO object'
|
|
317
|
-
end
|
|
318
|
-
rescue Errno::ENOENT
|
|
319
|
-
raise DBF::FileNotFoundError, "file not found: #{data}"
|
|
320
|
-
end
|
|
321
|
-
|
|
322
|
-
def open_memo(data, memo = nil) # :nodoc:
|
|
323
|
-
if memo
|
|
324
|
-
meth = memo.is_a?(StringIO) ? :new : :open
|
|
325
|
-
memo_class.send(meth, memo, version)
|
|
326
|
-
elsif !data.is_a?(StringIO)
|
|
327
|
-
files = Dir.glob(memo_search_path(data))
|
|
328
|
-
files.any? ? memo_class.open(files.first, version) : nil
|
|
194
|
+
Header.new(@data.read(VersionConfig::DBASE3_HEADER_SIZE))
|
|
329
195
|
end
|
|
330
196
|
end
|
|
331
197
|
|
data/lib/dbf/version.rb
CHANGED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBF
|
|
4
|
+
class VersionConfig
|
|
5
|
+
DBASE2_HEADER_SIZE = 8
|
|
6
|
+
DBASE3_HEADER_SIZE = 32
|
|
7
|
+
DBASE7_HEADER_SIZE = 68
|
|
8
|
+
|
|
9
|
+
VERSIONS = {
|
|
10
|
+
'02' => 'FoxBase',
|
|
11
|
+
'03' => 'dBase III without memo file',
|
|
12
|
+
'04' => 'dBase IV without memo file',
|
|
13
|
+
'05' => 'dBase V without memo file',
|
|
14
|
+
'07' => 'Visual Objects 1.x',
|
|
15
|
+
'30' => 'Visual FoxPro',
|
|
16
|
+
'32' => 'Visual FoxPro with field type Varchar or Varbinary',
|
|
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',
|
|
20
|
+
'7b' => 'dBase IV with memo file',
|
|
21
|
+
'83' => 'dBase III with memo file',
|
|
22
|
+
'87' => 'Visual Objects 1.x with memo file',
|
|
23
|
+
'8b' => 'dBase IV with memo file',
|
|
24
|
+
'8c' => 'dBase 7',
|
|
25
|
+
'8e' => 'dBase IV with SQL table',
|
|
26
|
+
'cb' => 'dBASE IV SQL table files, with memo',
|
|
27
|
+
'f5' => 'FoxPro with memo file',
|
|
28
|
+
'fb' => 'FoxPro without memo file'
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
FOXPRO_VERSIONS = {
|
|
32
|
+
'30' => 'Visual FoxPro',
|
|
33
|
+
'31' => 'Visual FoxPro with AutoIncrement field',
|
|
34
|
+
'f5' => 'FoxPro with memo file',
|
|
35
|
+
'fb' => 'FoxPro without memo file'
|
|
36
|
+
}.freeze
|
|
37
|
+
|
|
38
|
+
attr_reader :version
|
|
39
|
+
|
|
40
|
+
def initialize(version)
|
|
41
|
+
@version = version
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def version_description
|
|
45
|
+
VERSIONS[version]
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def header_size
|
|
49
|
+
case version
|
|
50
|
+
when '02'
|
|
51
|
+
DBASE2_HEADER_SIZE
|
|
52
|
+
when '04', '8c'
|
|
53
|
+
DBASE7_HEADER_SIZE
|
|
54
|
+
else
|
|
55
|
+
DBASE3_HEADER_SIZE
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def foxpro?
|
|
60
|
+
FOXPRO_VERSIONS.key?(version)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def memo_class
|
|
64
|
+
if foxpro?
|
|
65
|
+
Memo::Foxpro
|
|
66
|
+
else
|
|
67
|
+
version == '83' ? Memo::Dbase3 : Memo::Dbase4
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def read_column_args(table, io)
|
|
72
|
+
case version
|
|
73
|
+
when '02' then [table, *io.read(header_size * 2).unpack('A11 a C'), 0]
|
|
74
|
+
when '04', '8c' then [table, *io.read(48).unpack('A32 a C C x13')]
|
|
75
|
+
else [table, *io.read(header_size).unpack('A11 a x4 C2')]
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
data/lib/dbf.rb
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
1
3
|
require 'csv'
|
|
2
4
|
require 'date'
|
|
3
5
|
require 'forwardable'
|
|
@@ -6,11 +8,17 @@ require 'time'
|
|
|
6
8
|
|
|
7
9
|
require 'dbf/version'
|
|
8
10
|
require 'dbf/schema'
|
|
11
|
+
require 'dbf/find'
|
|
9
12
|
require 'dbf/record'
|
|
13
|
+
require 'dbf/record_context'
|
|
10
14
|
require 'dbf/column_type'
|
|
11
15
|
require 'dbf/column'
|
|
12
16
|
require 'dbf/encodings'
|
|
13
17
|
require 'dbf/header'
|
|
18
|
+
require 'dbf/version_config'
|
|
19
|
+
require 'dbf/file_handler'
|
|
20
|
+
require 'dbf/column_builder'
|
|
21
|
+
require 'dbf/record_iterator'
|
|
14
22
|
require 'dbf/table'
|
|
15
23
|
require 'dbf/memo/base'
|
|
16
24
|
require 'dbf/memo/dbase3'
|