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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7d565a51490240e9f5ed724970a568f12641b595b66e426a3a4a1a6ba0c84003
4
- data.tar.gz: 12fc66ce2b83c55157532d252b52eb6a343174898a7965a636beb7877aa29c17
3
+ metadata.gz: 96b3352f6c6d2cc3a667c6f390021c4f99be7c83ff2b9c911ac38c75d4c261aa
4
+ data.tar.gz: b787cd0e14a34ea66e670f88e793bcbebc1ccbdc260721da08ccee5d3de2624a
5
5
  SHA512:
6
- metadata.gz: c2de237f84ddc71f271a782b537f03d10a8586e939ad08f66b34c9f6bcc38a1b34aa4a2899d3248a1ff2f574bfb9920c19d2f134cae01ac66f0e228acd9a1c13
7
- data.tar.gz: 5b5b3ee469e5afe32bc54ea4f494b543c675a2b4c61b6e6cf56af68ab2ab989a35644b7795a29e1cd9e4dfe8e4129c1abf85696a4ff7df8fb0444f89c7f80193
6
+ metadata.gz: 52ceab2c71a3093ff691be23a0a0590e4309fd88218e683032e12805585dea05dd97bebd0170f003e7678641810d5d9aa6b9731bb0af647a8508d015875312d2
7
+ data.tar.gz: 41b87283de2deea377a3c27436d621486af57bc0421e5bc84dc1bd1f304abf8d264d72d8dda80350666895ca604efd335b829fd1efc79720721027146fe56b7d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.2.0
4
+
5
+ - Drop support for Ruby 3.1 and 3.2
6
+
3
7
  ## 5.1.1
4
8
 
5
9
  - Frozen string literals
data/README.md CHANGED
@@ -16,6 +16,8 @@ DBF is a small, fast Ruby library for reading dBase, xBase, Clipper, and FoxPro
16
16
  subject line
17
17
  * Change log: <https://github.com/infused/dbf/blob/main/CHANGELOG.md>
18
18
 
19
+ NOTE: Beginning with version 5.2 we have dropped support for Ruby 3.2 and earlier.
20
+
19
21
  NOTE: Beginning with version 4.3 we have dropped support for Ruby 3.0 and earlier.
20
22
 
21
23
  NOTE: Beginning with version 4 we have dropped support for Ruby 2.0, 2.1, 2.2, and 2.3. If you need support for these older Rubies,
@@ -28,7 +30,7 @@ please use 2.0.x (<https://github.com/infused/dbf/tree/2_stable>)
28
30
 
29
31
  DBF is tested to work with the following versions of Ruby:
30
32
 
31
- * Ruby 3.1.x, 3.2.x, 3.3.x
33
+ * Ruby 3.3.x, 3.4.x, 4.0.x
32
34
 
33
35
  ## Installation
34
36
 
@@ -251,7 +253,7 @@ class Book < Sequel::Model; end
251
253
  Sequel.migration do
252
254
  up do
253
255
  table = DBF::Table.new('db/dbf/books.dbf')
254
- eval(table.schema(:sequel, true)) # passing true to limit output to create_table() only
256
+ eval(table.schema(:sequel, table_only: true)) # limit output to create_table() only
255
257
 
256
258
  Book.reset_column_information
257
259
  table.each do |record|
data/dbf.gemspec CHANGED
@@ -1,24 +1,21 @@
1
- lib = File.expand_path('lib', __dir__)
2
- $LOAD_PATH.unshift lib unless $LOAD_PATH.include?(lib)
3
- require 'dbf/version'
1
+ require_relative 'lib/dbf/version'
4
2
 
5
3
  Gem::Specification.new do |s|
6
4
  s.name = 'dbf'
7
5
  s.version = DBF::VERSION
8
6
  s.authors = ['Keith Morrison']
9
7
  s.email = 'keithm@infused.org'
10
- s.homepage = 'http://github.com/infused/dbf'
8
+ s.homepage = 'https://github.com/infused/dbf'
11
9
  s.summary = 'Read xBase files'
12
10
  s.description = 'A small fast library for reading dBase, xBase, Clipper and FoxPro database files.'
13
11
  s.license = 'MIT'
14
12
  s.bindir = 'bin'
15
13
  s.executables = ['dbf']
16
- s.rdoc_options = ['--charset=UTF-8']
17
- s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'LICENSE']
18
- s.files = Dir['README.md', 'CHANGELOG.md', 'LICENSE', '{bin,lib,spec}/**/*', 'dbf.gemspec']
14
+ s.files = Dir['README.md', 'CHANGELOG.md', 'LICENSE', '{bin,lib}/**/*', 'dbf.gemspec']
19
15
  s.require_paths = ['lib']
20
- s.required_rubygems_version = Gem::Requirement.new('>= 3.2.3')
21
- s.required_ruby_version = Gem::Requirement.new('>= 3.1.0')
16
+ s.required_ruby_version = '>= 3.3.0'
22
17
  s.metadata['rubygems_mfa_required'] = 'true'
18
+ s.metadata['source_code_uri'] = 'https://github.com/infused/dbf'
19
+ s.metadata['changelog_uri'] = 'https://github.com/infused/dbf/blob/main/CHANGELOG.md'
23
20
  s.add_dependency 'csv'
24
21
  end
data/lib/dbf/column.rb CHANGED
@@ -2,17 +2,13 @@
2
2
 
3
3
  module DBF
4
4
  class Column
5
- extend Forwardable
6
-
7
5
  class LengthError < StandardError
8
6
  end
9
7
 
10
8
  class NameError < StandardError
11
9
  end
12
10
 
13
- attr_reader :table, :name, :type, :length, :decimal, :encoding
14
-
15
- def_delegator :type_cast_class, :type_cast
11
+ attr_reader :name, :type, :length, :decimal
16
12
 
17
13
  # rubocop:disable Style/MutableConstant
18
14
  TYPE_CAST_CLASS = {
@@ -26,7 +22,7 @@ module DBF
26
22
  M: ColumnType::Memo,
27
23
  B: ColumnType::Double,
28
24
  G: ColumnType::General,
29
- :+ => ColumnType::SignedLong2
25
+ :+ => ColumnType::AutoIncrement
30
26
  }
31
27
  # rubocop:enable Style/MutableConstant
32
28
  TYPE_CAST_CLASS.default = ColumnType::String
@@ -40,31 +36,37 @@ module DBF
40
36
  # @param length [Integer]
41
37
  # @param decimal [Integer]
42
38
  def initialize(table, name, type, length, decimal)
43
- @encoding = table.encoding
44
-
45
39
  @table = table
46
40
  @name = clean(name)
47
41
  @type = type
48
42
  @length = length
49
43
  @decimal = decimal
50
- @version = table.version
51
44
 
52
45
  validate_length
53
46
  validate_name
54
47
  end
55
48
 
56
- # Returns true if the column is a memo
49
+ def encoding = @table.encoding
50
+
51
+ # @param value [String]
52
+ def type_cast(value)
53
+ type_cast_class.type_cast(value)
54
+ end
55
+
56
+ # Decodes a raw column value, handling memo, blank, and type cast cases
57
57
  #
58
- # @return [Boolean]
59
- def memo?
60
- @memo ||= type == 'M'
58
+ # @param raw [String]
59
+ # @yield [raw] for memo column resolution
60
+ # @return decoded value
61
+ def decode(raw, &memo_handler)
62
+ type_cast_class.decode(raw, &memo_handler)
61
63
  end
62
64
 
63
65
  # Returns a Hash with :name, :type, :length, and :decimal keys
64
66
  #
65
67
  # @return [Hash]
66
68
  def to_hash
67
- {name: name, type: type, length: length, decimal: decimal}
69
+ {name:, type:, length:, decimal:}
68
70
  end
69
71
 
70
72
  # Underscored name
@@ -80,7 +82,7 @@ module DBF
80
82
  private
81
83
 
82
84
  def clean(value) # :nodoc:
83
- table.encode_string(value.strip.partition("\x00").first)
85
+ @table.encode_string(value.strip.split("\x00", 2).first || +'')
84
86
  end
85
87
 
86
88
  def type_cast_class # :nodoc:
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DBF
4
+ class ColumnBuilder
5
+ def initialize(table, data, version_config)
6
+ @table = table
7
+ @data = data
8
+ @version_config = version_config
9
+ end
10
+
11
+ def build
12
+ safe_seek do
13
+ @data.seek(@version_config.header_size)
14
+ [].tap do |columns|
15
+ columns << Column.new(*@version_config.read_column_args(@table, @data)) until end_of_record?
16
+ end
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def end_of_record?
23
+ safe_seek { @data.read(1).ord == 13 }
24
+ end
25
+
26
+ def safe_seek
27
+ original_pos = @data.pos
28
+ yield.tap { @data.seek(original_pos) }
29
+ end
30
+ end
31
+ end
@@ -11,6 +11,22 @@ module DBF
11
11
  @decimal = column.decimal
12
12
  @encoding = column.encoding
13
13
  end
14
+
15
+ def blank_value
16
+ nil
17
+ end
18
+
19
+ def skip_blank?
20
+ false
21
+ end
22
+
23
+ def decode(raw, &_memo_handler)
24
+ if skip_blank? && raw.count(' ') == raw.length
25
+ blank_value
26
+ else
27
+ type_cast(raw)
28
+ end
29
+ end
14
30
  end
15
31
 
16
32
  class Nil < Base
@@ -21,11 +37,13 @@ module DBF
21
37
  end
22
38
 
23
39
  class Number < Base
40
+ def skip_blank? = true
41
+
24
42
  # @param value [String]
25
43
  def type_cast(value)
26
- return nil if value.strip.empty?
44
+ return nil if value.empty?
27
45
 
28
- @decimal.zero? ? value.to_i : value.to_f
46
+ decimal.zero? ? value.to_i : value.to_f
29
47
  end
30
48
  end
31
49
 
@@ -43,12 +61,12 @@ module DBF
43
61
  end
44
62
  end
45
63
 
46
- class SignedLong2 < Base
64
+ class AutoIncrement < Base
47
65
  # @param value [String]
48
66
  def type_cast(value)
49
- s = value.unpack1('B*')
50
- sign_multiplier = s[0] == '0' ? -1 : 1
51
- s[1, 31].to_i(2) * sign_multiplier
67
+ bits = value.unpack1('B*')
68
+ sign_multiplier = bits[0] == '0' ? -1 : 1
69
+ bits[1, 31].to_i(2) * sign_multiplier
52
70
  end
53
71
  end
54
72
 
@@ -67,13 +85,20 @@ module DBF
67
85
  end
68
86
 
69
87
  class Boolean < Base
88
+ def skip_blank? = true
89
+ def blank_value = false
90
+
70
91
  # @param value [String]
71
92
  def type_cast(value)
72
- value.strip.match?(/^(y|t)$/i)
93
+ byte = value.getbyte(0)
94
+ byte == 89 || byte == 121 || byte == 84 || byte == 116 # Y y T t
73
95
  end
74
96
  end
75
97
 
76
98
  class Date < Base
99
+ def skip_blank? = true
100
+ def blank_value = false
101
+
77
102
  # @param value [String]
78
103
  def type_cast(value)
79
104
  value.match?(/\d{8}/) && ::Date.strptime(value, '%Y%m%d')
@@ -94,13 +119,16 @@ module DBF
94
119
  end
95
120
 
96
121
  class Memo < Base
122
+ def decode(raw, &memo_handler)
123
+ memo_content = memo_handler.call(raw)
124
+ memo_content ? type_cast(memo_content) : nil
125
+ end
126
+
97
127
  # @param value [String]
98
128
  def type_cast(value)
99
- if encoding && !value.nil?
100
- value.dup.force_encoding(@encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace)
101
- else
102
- value
103
- end
129
+ return value unless encoding && value
130
+
131
+ value.dup.force_encoding(encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace)
104
132
  end
105
133
  end
106
134
 
@@ -112,10 +140,26 @@ module DBF
112
140
  end
113
141
 
114
142
  class String < Base
143
+ def initialize(column)
144
+ super
145
+ @target_encoding = Encoding.default_external
146
+ @needs_encode = encoding && encoding != @target_encoding
147
+ end
148
+
149
+ def skip_blank? = true
150
+ def blank_value = ''
151
+
115
152
  # @param value [String]
116
153
  def type_cast(value)
117
- value = value.strip
118
- @encoding ? value.force_encoding(@encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace) : value
154
+ value.strip!
155
+ encoding ? encode(value) : value
156
+ end
157
+
158
+ private
159
+
160
+ def encode(value)
161
+ value.force_encoding(encoding)
162
+ @needs_encode ? value.encode(@target_encoding, undef: :replace, invalid: :replace) : value
119
163
  end
120
164
  end
121
165
  end
@@ -26,7 +26,7 @@ module DBF
26
26
  @db = DBF::Table.new(@path)
27
27
  @tables = extract_dbc_data
28
28
  rescue Errno::ENOENT
29
- raise DBF::FileNotFoundError, "file not found: #{data}"
29
+ raise DBF::FileNotFoundError, "file not found: #{path}"
30
30
  end
31
31
 
32
32
  def table_names
@@ -38,9 +38,7 @@ module DBF
38
38
  # @param name [String]
39
39
  # @return [DBF::Table]
40
40
  def table(name)
41
- Table.new table_path(name) do |table|
42
- table.long_names = @tables[name]
43
- end
41
+ Table.new(table_path(name), long_names: @tables[name])
44
42
  end
45
43
 
46
44
  # Searches the database directory for the table's dbf file
@@ -58,7 +56,8 @@ module DBF
58
56
  end
59
57
 
60
58
  def method_missing(method, *args) # :nodoc:
61
- table_names.index(method.to_s) ? table(method.to_s) : super
59
+ name = method.to_s
60
+ table_names.index(name) ? table(name) : super
62
61
  end
63
62
 
64
63
  def respond_to_missing?(method, *)
@@ -72,45 +71,33 @@ module DBF
72
71
  # are in the same order as in the linked tables but only the long name
73
72
  # is provided.
74
73
  def extract_dbc_data # :nodoc:
75
- data = {}
76
- @db.each do |record|
74
+ build_table_data.values.to_h { |entry| entry.values_at(:name, :fields) }
75
+ end
76
+
77
+ def build_table_data # :nodoc:
78
+ @db.each_with_object({}) do |record, hash|
77
79
  next unless record
78
80
 
81
+ name = record.objectname
79
82
  case record.objecttype
80
- when 'Table'
81
- # This is a related table
82
- process_table record, data
83
- when 'Field'
84
- # This is a related field. The parentid points to the table object.
85
- # Create using the parentid if the parentid is still unknown.
86
- process_field record, data
83
+ when 'Table' then hash[record.objectid] = table_field_hash(name)
84
+ when 'Field' then (hash[record.parentid] ||= table_field_hash('UNKNOWN'))[:fields] << name
87
85
  end
88
86
  end
89
-
90
- data.values.to_h { |v| [v[:name], v[:fields]] }
91
- end
92
-
93
- def process_table(record, data)
94
- id = record.objectid
95
- name = record.objectname
96
- data[id] = table_field_hash(name)
97
- end
98
-
99
- def process_field(record, data)
100
- id = record.parentid
101
- name = 'UNKNOWN'
102
- field = record.objectname
103
- data[id] ||= table_field_hash(name)
104
- data[id][:fields] << field
105
87
  end
106
88
 
107
89
  def table_field_hash(name)
108
- {name: name, fields: []}
90
+ {name:, fields: []}
109
91
  end
110
92
  end
111
93
 
112
94
  class Table < DBF::Table
113
- attr_accessor :long_names
95
+ attr_reader :long_names
96
+
97
+ def initialize(path, long_names:)
98
+ @long_names = long_names
99
+ super(path)
100
+ end
114
101
 
115
102
  def build_columns # :nodoc:
116
103
  columns = super
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DBF
4
+ module FileHandler
5
+ module_function
6
+
7
+ def open_data(data)
8
+ case data
9
+ when StringIO
10
+ data
11
+ when String
12
+ File.open(data, 'rb')
13
+ else
14
+ raise ArgumentError, 'data must be a file path or StringIO object'
15
+ end
16
+ rescue Errno::ENOENT
17
+ raise DBF::FileNotFoundError, "file not found: #{data}"
18
+ end
19
+
20
+ def open_memo(data, memo, memo_class, version)
21
+ if memo
22
+ meth = memo.is_a?(StringIO) ? :new : :open
23
+ memo_class.send(meth, memo, version)
24
+ elsif !data.is_a?(StringIO)
25
+ path = Dir.glob(memo_search_path(data)).first
26
+ path && memo_class.open(path, version)
27
+ end
28
+ end
29
+
30
+ def memo_search_path(io)
31
+ dirname = File.dirname(io)
32
+ basename = File.basename(io, '.*')
33
+ "#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}"
34
+ end
35
+ end
36
+ end
data/lib/dbf/find.rb ADDED
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DBF
4
+ # The Find module provides methods for searching and retrieving
5
+ # records using a simple ActiveRecord-like syntax.
6
+ #
7
+ # Examples:
8
+ # table = DBF::Table.new 'mydata.dbf'
9
+ #
10
+ # # Find record number 5
11
+ # table.find(5)
12
+ #
13
+ # # Find all records for Keith Morrison
14
+ # table.find :all, first_name: "Keith", last_name: "Morrison"
15
+ #
16
+ # # Find first record
17
+ # table.find :first, first_name: "Keith"
18
+ #
19
+ # The <b>command</b> may be a record index, :all, or :first.
20
+ # <b>options</b> is optional and, if specified, should be a hash where the
21
+ # keys correspond to column names in the database. The values will be
22
+ # matched exactly with the value in the database. If you specify more
23
+ # than one key, all values must match in order for the record to be
24
+ # returned. The equivalent SQL would be "WHERE key1 = 'value1'
25
+ # AND key2 = 'value2'".
26
+ module Find
27
+ # @param command [Integer, Symbol] command
28
+ # @param options [optional, Hash] options Hash of search parameters
29
+ # @yield [optional, DBF::Record, NilClass]
30
+ def find(command, options = {}, &)
31
+ case command
32
+ when Integer then record(command)
33
+ when Array then command.map { |index| record(index) }
34
+ when :all then find_all_records(options, &)
35
+ when :first then find_first_record(options)
36
+ end
37
+ end
38
+
39
+ private
40
+
41
+ def find_all_records(options)
42
+ select do |record|
43
+ next unless record&.match?(options)
44
+
45
+ yield record if block_given?
46
+ record
47
+ end
48
+ end
49
+
50
+ def find_first_record(options)
51
+ detect { |record| record&.match?(options) }
52
+ end
53
+ end
54
+ end
data/lib/dbf/header.rb CHANGED
@@ -5,19 +5,16 @@ module DBF
5
5
  attr_reader :version, :record_count, :header_length, :record_length, :encoding_key, :encoding
6
6
 
7
7
  def initialize(data)
8
- @data = data
9
- unpack_header
10
- end
11
-
12
- def unpack_header
13
- @version = @data.unpack1('H2')
8
+ @version = data.unpack1('H2')
9
+ @encoding_key = nil
10
+ @encoding = nil
14
11
 
15
12
  case @version
16
13
  when '02'
17
- @record_count, @record_length = @data.unpack('x v x3 v')
14
+ @record_count, @record_length = data.unpack('x v x3 v')
18
15
  @header_length = 521
19
16
  else
20
- @record_count, @header_length, @record_length, @encoding_key = @data.unpack('x x3 V v2 x17 H2')
17
+ @record_count, @header_length, @record_length, @encoding_key = data.unpack('x x3 V v2 x17 H2')
21
18
  @encoding = DBF::ENCODINGS[@encoding_key]
22
19
  end
23
20
  end
data/lib/dbf/memo/base.rb CHANGED
@@ -31,6 +31,8 @@ module DBF
31
31
 
32
32
  private
33
33
 
34
+ attr_reader :data
35
+
34
36
  def offset(start_block) # :nodoc:
35
37
  start_block * block_size
36
38
  end
@@ -4,10 +4,10 @@ module DBF
4
4
  module Memo
5
5
  class Dbase3 < Base
6
6
  def build_memo(start_block) # :nodoc:
7
- @data.seek offset(start_block)
7
+ data.seek offset(start_block)
8
8
  memo_string = +''
9
9
  loop do
10
- block = @data.read(BLOCK_SIZE).gsub(/(\000|\032)/, '')
10
+ block = data.read(BLOCK_SIZE).gsub(/(\000|\032)/, '')
11
11
  memo_string << block
12
12
  break if block.size < BLOCK_SIZE
13
13
  end
@@ -4,8 +4,8 @@ module DBF
4
4
  module Memo
5
5
  class Dbase4 < Base
6
6
  def build_memo(start_block) # :nodoc:
7
- @data.seek offset(start_block)
8
- @data.read(@data.read(BLOCK_HEADER_SIZE).unpack1('x4L'))
7
+ data.seek offset(start_block)
8
+ data.read(data.read(BLOCK_HEADER_SIZE).unpack1('x4L'))
9
9
  end
10
10
  end
11
11
  end
@@ -5,24 +5,31 @@ module DBF
5
5
  class Foxpro < Base
6
6
  FPT_HEADER_SIZE = 512
7
7
 
8
+ def initialize(data, version)
9
+ @data = data
10
+ super
11
+ end
12
+
8
13
  def build_memo(start_block) # :nodoc:
9
14
  @data.seek offset(start_block)
10
-
11
15
  memo_type, memo_size, memo_string = @data.read(block_size).unpack('NNa*')
12
16
  return nil unless memo_type == 1 && memo_size > 0
13
17
 
14
- if memo_size > block_content_size
15
- memo_string << @data.read(content_size(memo_size))
16
- else
17
- memo_string = memo_string[0, memo_size]
18
- end
19
- memo_string
18
+ read_memo_content(memo_string, memo_size)
20
19
  rescue StandardError
21
20
  nil
22
21
  end
23
22
 
24
23
  private
25
24
 
25
+ def read_memo_content(memo_string, memo_size) # :nodoc:
26
+ if memo_size > block_content_size
27
+ memo_string << @data.read(content_size(memo_size))
28
+ else
29
+ memo_string[0, memo_size]
30
+ end
31
+ end
32
+
26
33
  def block_size # :nodoc:
27
34
  @block_size ||= begin
28
35
  @data.rewind