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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ea8e27ddd0d6d12797336f59d245bc8b62245e59c8fad0ecbf12c97513ac244f
4
- data.tar.gz: d3cc1955ebb54357b5c724ef1b8687a8f69a47f794c445a08840d5dc08fa5861
3
+ metadata.gz: 96b3352f6c6d2cc3a667c6f390021c4f99be7c83ff2b9c911ac38c75d4c261aa
4
+ data.tar.gz: b787cd0e14a34ea66e670f88e793bcbebc1ccbdc260721da08ccee5d3de2624a
5
5
  SHA512:
6
- metadata.gz: 87ec87d1e3a45f4ec1f787f4e56d64aad00779aebe5e391656fe457f278219ffa72de97cc4ae679d2b8a14c7f03035fb059397251a290bc21587513382207c82
7
- data.tar.gz: a931c1aa3ab7a5128824cb43bd4ec03217a52d548a071209495dce1647f68f269c78f84c15534ecf35ed9badac227e96a46ab53e0f33a9271d440d832fd26959
6
+ metadata.gz: 52ceab2c71a3093ff691be23a0a0590e4309fd88218e683032e12805585dea05dd97bebd0170f003e7678641810d5d9aa6b9731bb0af647a8508d015875312d2
7
+ data.tar.gz: 41b87283de2deea377a3c27436d621486af57bc0421e5bc84dc1bd1f304abf8d264d72d8dda80350666895ca604efd335b829fd1efc79720721027146fe56b7d
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.2.0
4
+
5
+ - Drop support for Ruby 3.1 and 3.2
6
+
7
+ ## 5.1.1
8
+
9
+ - Frozen string literals
10
+
3
11
  ## 5.1.0
4
12
 
5
13
  - Drop support for Ruby 3.0.x
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
@@ -1,16 +1,14 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DBF
2
4
  class Column
3
- extend Forwardable
4
-
5
5
  class LengthError < StandardError
6
6
  end
7
7
 
8
8
  class NameError < StandardError
9
9
  end
10
10
 
11
- attr_reader :table, :name, :type, :length, :decimal, :encoding
12
-
13
- def_delegator :type_cast_class, :type_cast
11
+ attr_reader :name, :type, :length, :decimal
14
12
 
15
13
  # rubocop:disable Style/MutableConstant
16
14
  TYPE_CAST_CLASS = {
@@ -24,7 +22,7 @@ module DBF
24
22
  M: ColumnType::Memo,
25
23
  B: ColumnType::Double,
26
24
  G: ColumnType::General,
27
- :+ => ColumnType::SignedLong2
25
+ :+ => ColumnType::AutoIncrement
28
26
  }
29
27
  # rubocop:enable Style/MutableConstant
30
28
  TYPE_CAST_CLASS.default = ColumnType::String
@@ -38,31 +36,37 @@ module DBF
38
36
  # @param length [Integer]
39
37
  # @param decimal [Integer]
40
38
  def initialize(table, name, type, length, decimal)
41
- @encoding = table.encoding
42
-
43
39
  @table = table
44
40
  @name = clean(name)
45
41
  @type = type
46
42
  @length = length
47
43
  @decimal = decimal
48
- @version = table.version
49
44
 
50
45
  validate_length
51
46
  validate_name
52
47
  end
53
48
 
54
- # 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
55
57
  #
56
- # @return [Boolean]
57
- def memo?
58
- @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)
59
63
  end
60
64
 
61
65
  # Returns a Hash with :name, :type, :length, and :decimal keys
62
66
  #
63
67
  # @return [Hash]
64
68
  def to_hash
65
- {name: name, type: type, length: length, decimal: decimal}
69
+ {name:, type:, length:, decimal:}
66
70
  end
67
71
 
68
72
  # Underscored name
@@ -78,7 +82,7 @@ module DBF
78
82
  private
79
83
 
80
84
  def clean(value) # :nodoc:
81
- table.encode_string(value.strip.partition("\x00").first)
85
+ @table.encode_string(value.strip.split("\x00", 2).first || +'')
82
86
  end
83
87
 
84
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DBF
2
4
  module ColumnType
3
5
  class Base
@@ -9,6 +11,22 @@ module DBF
9
11
  @decimal = column.decimal
10
12
  @encoding = column.encoding
11
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
12
30
  end
13
31
 
14
32
  class Nil < Base
@@ -19,11 +37,13 @@ module DBF
19
37
  end
20
38
 
21
39
  class Number < Base
40
+ def skip_blank? = true
41
+
22
42
  # @param value [String]
23
43
  def type_cast(value)
24
- return nil if value.strip.empty?
44
+ return nil if value.empty?
25
45
 
26
- @decimal.zero? ? value.to_i : value.to_f
46
+ decimal.zero? ? value.to_i : value.to_f
27
47
  end
28
48
  end
29
49
 
@@ -41,12 +61,12 @@ module DBF
41
61
  end
42
62
  end
43
63
 
44
- class SignedLong2 < Base
64
+ class AutoIncrement < Base
45
65
  # @param value [String]
46
66
  def type_cast(value)
47
- s = value.unpack1('B*')
48
- sign_multiplier = s[0] == '0' ? -1 : 1
49
- 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
50
70
  end
51
71
  end
52
72
 
@@ -65,13 +85,20 @@ module DBF
65
85
  end
66
86
 
67
87
  class Boolean < Base
88
+ def skip_blank? = true
89
+ def blank_value = false
90
+
68
91
  # @param value [String]
69
92
  def type_cast(value)
70
- value.strip.match?(/^(y|t)$/i)
93
+ byte = value.getbyte(0)
94
+ byte == 89 || byte == 121 || byte == 84 || byte == 116 # Y y T t
71
95
  end
72
96
  end
73
97
 
74
98
  class Date < Base
99
+ def skip_blank? = true
100
+ def blank_value = false
101
+
75
102
  # @param value [String]
76
103
  def type_cast(value)
77
104
  value.match?(/\d{8}/) && ::Date.strptime(value, '%Y%m%d')
@@ -92,28 +119,47 @@ module DBF
92
119
  end
93
120
 
94
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
+
95
127
  # @param value [String]
96
128
  def type_cast(value)
97
- if encoding && !value.nil?
98
- value.force_encoding(@encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace)
99
- else
100
- value
101
- end
129
+ return value unless encoding && value
130
+
131
+ value.dup.force_encoding(encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace)
102
132
  end
103
133
  end
104
134
 
105
135
  class General < Base
106
136
  # @param value [String]
107
137
  def type_cast(value)
108
- value
138
+ value&.dup&.force_encoding(Encoding::ASCII_8BIT)
109
139
  end
110
140
  end
111
141
 
112
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
+
113
152
  # @param value [String]
114
153
  def type_cast(value)
115
- value = value.strip
116
- @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
117
163
  end
118
164
  end
119
165
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DBF
2
4
  # DBF::Database::Foxpro is the primary interface to a Visual Foxpro database
3
5
  # container (.dbc file). When using this database container, long fieldnames
@@ -24,7 +26,7 @@ module DBF
24
26
  @db = DBF::Table.new(@path)
25
27
  @tables = extract_dbc_data
26
28
  rescue Errno::ENOENT
27
- raise DBF::FileNotFoundError, "file not found: #{data}"
29
+ raise DBF::FileNotFoundError, "file not found: #{path}"
28
30
  end
29
31
 
30
32
  def table_names
@@ -36,9 +38,7 @@ module DBF
36
38
  # @param name [String]
37
39
  # @return [DBF::Table]
38
40
  def table(name)
39
- Table.new table_path(name) do |table|
40
- table.long_names = @tables[name]
41
- end
41
+ Table.new(table_path(name), long_names: @tables[name])
42
42
  end
43
43
 
44
44
  # Searches the database directory for the table's dbf file
@@ -56,7 +56,8 @@ module DBF
56
56
  end
57
57
 
58
58
  def method_missing(method, *args) # :nodoc:
59
- table_names.index(method.to_s) ? table(method.to_s) : super
59
+ name = method.to_s
60
+ table_names.index(name) ? table(name) : super
60
61
  end
61
62
 
62
63
  def respond_to_missing?(method, *)
@@ -70,45 +71,33 @@ module DBF
70
71
  # are in the same order as in the linked tables but only the long name
71
72
  # is provided.
72
73
  def extract_dbc_data # :nodoc:
73
- data = {}
74
- @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|
75
79
  next unless record
76
80
 
81
+ name = record.objectname
77
82
  case record.objecttype
78
- when 'Table'
79
- # This is a related table
80
- process_table record, data
81
- when 'Field'
82
- # This is a related field. The parentid points to the table object.
83
- # Create using the parentid if the parentid is still unknown.
84
- 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
85
85
  end
86
86
  end
87
-
88
- data.values.to_h { |v| [v[:name], v[:fields]] }
89
- end
90
-
91
- def process_table(record, data)
92
- id = record.objectid
93
- name = record.objectname
94
- data[id] = table_field_hash(name)
95
- end
96
-
97
- def process_field(record, data)
98
- id = record.parentid
99
- name = 'UNKNOWN'
100
- field = record.objectname
101
- data[id] ||= table_field_hash(name)
102
- data[id][:fields] << field
103
87
  end
104
88
 
105
89
  def table_field_hash(name)
106
- {name: name, fields: []}
90
+ {name:, fields: []}
107
91
  end
108
92
  end
109
93
 
110
94
  class Table < DBF::Table
111
- 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
112
101
 
113
102
  def build_columns # :nodoc:
114
103
  columns = super
data/lib/dbf/encodings.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DBF
2
4
  ENCODINGS = {
3
5
  '01' => 'cp437', # U.S. MS-DOS
@@ -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
@@ -1,21 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DBF
2
4
  class Header
3
5
  attr_reader :version, :record_count, :header_length, :record_length, :encoding_key, :encoding
4
6
 
5
7
  def initialize(data)
6
- @data = data
7
- unpack_header
8
- end
9
-
10
- def unpack_header
11
- @version = @data.unpack1('H2')
8
+ @version = data.unpack1('H2')
9
+ @encoding_key = nil
10
+ @encoding = nil
12
11
 
13
12
  case @version
14
13
  when '02'
15
- @record_count, @record_length = @data.unpack('x v x3 v')
14
+ @record_count, @record_length = data.unpack('x v x3 v')
16
15
  @header_length = 521
17
16
  else
18
- @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')
19
18
  @encoding = DBF::ENCODINGS[@encoding_key]
20
19
  end
21
20
  end
data/lib/dbf/memo/base.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DBF
2
4
  module Memo
3
5
  class Base
@@ -29,6 +31,8 @@ module DBF
29
31
 
30
32
  private
31
33
 
34
+ attr_reader :data
35
+
32
36
  def offset(start_block) # :nodoc:
33
37
  start_block * block_size
34
38
  end
@@ -1,11 +1,13 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DBF
2
4
  module Memo
3
5
  class Dbase3 < Base
4
6
  def build_memo(start_block) # :nodoc:
5
- @data.seek offset(start_block)
6
- memo_string = ''
7
+ data.seek offset(start_block)
8
+ memo_string = +''
7
9
  loop do
8
- block = @data.read(BLOCK_SIZE).gsub(/(\000|\032)/, '')
10
+ block = data.read(BLOCK_SIZE).gsub(/(\000|\032)/, '')
9
11
  memo_string << block
10
12
  break if block.size < BLOCK_SIZE
11
13
  end
@@ -1,9 +1,11 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DBF
2
4
  module Memo
3
5
  class Dbase4 < Base
4
6
  def build_memo(start_block) # :nodoc:
5
- @data.seek offset(start_block)
6
- @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'))
7
9
  end
8
10
  end
9
11
  end
@@ -1,26 +1,35 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module DBF
2
4
  module Memo
3
5
  class Foxpro < Base
4
6
  FPT_HEADER_SIZE = 512
5
7
 
8
+ def initialize(data, version)
9
+ @data = data
10
+ super
11
+ end
12
+
6
13
  def build_memo(start_block) # :nodoc:
7
14
  @data.seek offset(start_block)
8
-
9
15
  memo_type, memo_size, memo_string = @data.read(block_size).unpack('NNa*')
10
16
  return nil unless memo_type == 1 && memo_size > 0
11
17
 
12
- if memo_size > block_content_size
13
- memo_string << @data.read(content_size(memo_size))
14
- else
15
- memo_string = memo_string[0, memo_size]
16
- end
17
- memo_string
18
+ read_memo_content(memo_string, memo_size)
18
19
  rescue StandardError
19
20
  nil
20
21
  end
21
22
 
22
23
  private
23
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
+
24
33
  def block_size # :nodoc:
25
34
  @block_size ||= begin
26
35
  @data.rewind