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