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.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +4 -0
  3. data/README.md +4 -2
  4. data/dbf.gemspec +6 -9
  5. data/lib/dbf/column.rb +17 -15
  6. data/lib/dbf/column_builder.rb +31 -0
  7. data/lib/dbf/column_type.rb +58 -14
  8. data/lib/dbf/database/foxpro.rb +19 -32
  9. data/lib/dbf/file_handler.rb +36 -0
  10. data/lib/dbf/find.rb +54 -0
  11. data/lib/dbf/header.rb +5 -8
  12. data/lib/dbf/memo/base.rb +2 -0
  13. data/lib/dbf/memo/dbase3.rb +2 -2
  14. data/lib/dbf/memo/dbase4.rb +2 -2
  15. data/lib/dbf/memo/foxpro.rb +14 -7
  16. data/lib/dbf/record.rb +60 -34
  17. data/lib/dbf/record_context.rb +5 -0
  18. data/lib/dbf/record_iterator.rb +35 -0
  19. data/lib/dbf/schema.rb +21 -21
  20. data/lib/dbf/table.rb +42 -178
  21. data/lib/dbf/version.rb +1 -1
  22. data/lib/dbf/version_config.rb +79 -0
  23. data/lib/dbf.rb +6 -0
  24. metadata +15 -64
  25. data/spec/dbf/column_spec.rb +0 -287
  26. data/spec/dbf/database/foxpro_spec.rb +0 -53
  27. data/spec/dbf/encoding_spec.rb +0 -49
  28. data/spec/dbf/file_formats_spec.rb +0 -221
  29. data/spec/dbf/record_spec.rb +0 -116
  30. data/spec/dbf/table_spec.rb +0 -377
  31. data/spec/fixtures/cp1251.dbf +0 -0
  32. data/spec/fixtures/cp1251_summary.txt +0 -12
  33. data/spec/fixtures/dbase_02.dbf +0 -0
  34. data/spec/fixtures/dbase_02_summary.txt +0 -23
  35. data/spec/fixtures/dbase_03.dbf +0 -0
  36. data/spec/fixtures/dbase_03_cyrillic.dbf +0 -0
  37. data/spec/fixtures/dbase_03_cyrillic_summary.txt +0 -11
  38. data/spec/fixtures/dbase_03_summary.txt +0 -40
  39. data/spec/fixtures/dbase_30.dbf +0 -0
  40. data/spec/fixtures/dbase_30.fpt +0 -0
  41. data/spec/fixtures/dbase_30_summary.txt +0 -154
  42. data/spec/fixtures/dbase_31.dbf +0 -0
  43. data/spec/fixtures/dbase_31_summary.txt +0 -20
  44. data/spec/fixtures/dbase_32.dbf +0 -0
  45. data/spec/fixtures/dbase_32_summary.txt +0 -11
  46. data/spec/fixtures/dbase_83.dbf +0 -0
  47. data/spec/fixtures/dbase_83.dbt +0 -0
  48. data/spec/fixtures/dbase_83_missing_memo.dbf +0 -0
  49. data/spec/fixtures/dbase_83_missing_memo_record_0.yml +0 -16
  50. data/spec/fixtures/dbase_83_record_0.yml +0 -16
  51. data/spec/fixtures/dbase_83_record_9.yml +0 -23
  52. data/spec/fixtures/dbase_83_schema_ar.txt +0 -19
  53. data/spec/fixtures/dbase_83_schema_sq.txt +0 -21
  54. data/spec/fixtures/dbase_83_schema_sq_lim.txt +0 -21
  55. data/spec/fixtures/dbase_83_summary.txt +0 -24
  56. data/spec/fixtures/dbase_8b.dbf +0 -0
  57. data/spec/fixtures/dbase_8b.dbt +0 -0
  58. data/spec/fixtures/dbase_8b_summary.txt +0 -15
  59. data/spec/fixtures/dbase_8c.dbf +0 -0
  60. data/spec/fixtures/dbase_f5.dbf +0 -0
  61. data/spec/fixtures/dbase_f5.fpt +0 -0
  62. data/spec/fixtures/dbase_f5_summary.txt +0 -68
  63. data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DBC +0 -0
  64. data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DCT +0 -0
  65. data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DCX +0 -0
  66. data/spec/fixtures/foxprodb/calls.CDX +0 -0
  67. data/spec/fixtures/foxprodb/calls.FPT +0 -0
  68. data/spec/fixtures/foxprodb/calls.dbf +0 -0
  69. data/spec/fixtures/foxprodb/contacts.CDX +0 -0
  70. data/spec/fixtures/foxprodb/contacts.FPT +0 -0
  71. data/spec/fixtures/foxprodb/contacts.dbf +0 -0
  72. data/spec/fixtures/foxprodb/setup.CDX +0 -0
  73. data/spec/fixtures/foxprodb/setup.dbf +0 -0
  74. data/spec/fixtures/foxprodb/types.CDX +0 -0
  75. data/spec/fixtures/foxprodb/types.dbf +0 -0
  76. data/spec/fixtures/polygon.dbf +0 -0
  77. 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 columns [Column]
10
- # @param version [String]
11
- # @param memo [DBF::Memo]
12
- def initialize(data, columns, version, memo)
13
- @data = StringIO.new(data)
14
- @columns = columns
15
- @version = version
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
- other.respond_to?(:attributes) && other.attributes == attributes
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 attributes.key?(key)
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 ||= @columns.map { |column| init_attribute(column) }
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 column_names # :nodoc:
64
- @column_names ||= @columns.map(&:name)
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 get_data(column) # :nodoc:
68
- @data.read(column.length)
93
+ def column_name_index(key) # :nodoc:
94
+ column_names.index(key) || underscored_column_names.index(key)
69
95
  end
70
96
 
71
- def get_memo(column) # :nodoc:
72
- if @memo
73
- @memo.get(memo_start_block(column))
74
- else
75
- # the memo file is missing, so read ahead to next record and return nil
76
- @data.read(column.length)
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 init_attribute(column) # :nodoc:
82
- value = column.memo? ? get_memo(column) : get_data(column)
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 memo_start_block(column) # :nodoc:
87
- data = get_data(column)
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
- if (index = underscored_column_names.index(method.to_s))
94
- attributes[@columns[index].name]
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DBF
4
+ RecordContext = Struct.new(:columns, :version, :memo, :column_offsets, keyword_init: true)
5
+ 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
- s = +"ActiveRecord::Schema.define do\n"
53
- s << " create_table \"#{name}\" do |t|\n"
57
+ output = +"ActiveRecord::Schema.define do\n"
58
+ output << " create_table \"#{name}\" do |t|\n"
54
59
  columns.each do |column|
55
- s << " t.column #{activerecord_schema_definition(column)}"
60
+ output << " t.column #{activerecord_schema_definition(column)}"
56
61
  end
57
- s << " end\nend"
58
- s
62
+ output << " end\nend"
63
+ output
59
64
  end
60
65
 
61
66
  def sequel_schema(table_only: false) # :nodoc:
62
- s = +''
63
- s << "Sequel.migration do\n" unless table_only
64
- s << " change do\n " unless table_only
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
- s << " column #{sequel_schema_definition(column)}"
71
+ output << " column #{sequel_schema_definition(column)}"
68
72
  end
69
- s << " end\n"
70
- s << " end\n" unless table_only
71
- s << "end\n" unless table_only
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
- case column.type
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[column.type]
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
- if format == :sequel
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
- DBASE2_HEADER_SIZE = 8
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
- @encoding = encoding || header.encoding || Encoding.default_external
82
- @memo = open_memo(data, memo)
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
- if @memo
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
- record_count.times { |i| yield record(i) }
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
- return unless @data.respond_to?(:path)
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, columns, version, @memo)
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
- out_io = path ? File.open(path, 'w') : $stdout
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
- VERSIONS[version]
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 build_columns # :nodoc:
234
- safe_seek do
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 find_all(options) # :nodoc:
274
- select do |record|
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 find_first(options) # :nodoc:
283
- detect { |record| record&.match?(options) }
182
+ def build_columns # :nodoc:
183
+ ColumnBuilder.new(self, @data, version_config).build
284
184
  end
285
185
 
286
- def foxpro? # :nodoc:
287
- FOXPRO_VERSIONS.key?(version)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DBF
4
- VERSION = '5.1.1'.freeze
4
+ VERSION = '5.2.0'
5
5
  end
@@ -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'