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