dbf 5.1.1 → 5.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +4 -2
- data/dbf.gemspec +6 -9
- data/lib/dbf/column.rb +17 -15
- data/lib/dbf/column_builder.rb +31 -0
- data/lib/dbf/column_type.rb +58 -14
- data/lib/dbf/database/foxpro.rb +19 -32
- data/lib/dbf/file_handler.rb +36 -0
- data/lib/dbf/find.rb +54 -0
- data/lib/dbf/header.rb +5 -8
- data/lib/dbf/memo/base.rb +2 -0
- data/lib/dbf/memo/dbase3.rb +2 -2
- data/lib/dbf/memo/dbase4.rb +2 -2
- data/lib/dbf/memo/foxpro.rb +14 -7
- data/lib/dbf/record.rb +60 -34
- data/lib/dbf/record_context.rb +5 -0
- data/lib/dbf/record_iterator.rb +35 -0
- data/lib/dbf/schema.rb +21 -21
- data/lib/dbf/table.rb +42 -178
- data/lib/dbf/version.rb +1 -1
- data/lib/dbf/version_config.rb +79 -0
- data/lib/dbf.rb +6 -0
- metadata +15 -64
- data/spec/dbf/column_spec.rb +0 -287
- data/spec/dbf/database/foxpro_spec.rb +0 -53
- data/spec/dbf/encoding_spec.rb +0 -49
- data/spec/dbf/file_formats_spec.rb +0 -221
- data/spec/dbf/record_spec.rb +0 -116
- data/spec/dbf/table_spec.rb +0 -377
- data/spec/fixtures/cp1251.dbf +0 -0
- data/spec/fixtures/cp1251_summary.txt +0 -12
- data/spec/fixtures/dbase_02.dbf +0 -0
- data/spec/fixtures/dbase_02_summary.txt +0 -23
- data/spec/fixtures/dbase_03.dbf +0 -0
- data/spec/fixtures/dbase_03_cyrillic.dbf +0 -0
- data/spec/fixtures/dbase_03_cyrillic_summary.txt +0 -11
- data/spec/fixtures/dbase_03_summary.txt +0 -40
- data/spec/fixtures/dbase_30.dbf +0 -0
- data/spec/fixtures/dbase_30.fpt +0 -0
- data/spec/fixtures/dbase_30_summary.txt +0 -154
- data/spec/fixtures/dbase_31.dbf +0 -0
- data/spec/fixtures/dbase_31_summary.txt +0 -20
- data/spec/fixtures/dbase_32.dbf +0 -0
- data/spec/fixtures/dbase_32_summary.txt +0 -11
- data/spec/fixtures/dbase_83.dbf +0 -0
- data/spec/fixtures/dbase_83.dbt +0 -0
- data/spec/fixtures/dbase_83_missing_memo.dbf +0 -0
- data/spec/fixtures/dbase_83_missing_memo_record_0.yml +0 -16
- data/spec/fixtures/dbase_83_record_0.yml +0 -16
- data/spec/fixtures/dbase_83_record_9.yml +0 -23
- data/spec/fixtures/dbase_83_schema_ar.txt +0 -19
- data/spec/fixtures/dbase_83_schema_sq.txt +0 -21
- data/spec/fixtures/dbase_83_schema_sq_lim.txt +0 -21
- data/spec/fixtures/dbase_83_summary.txt +0 -24
- data/spec/fixtures/dbase_8b.dbf +0 -0
- data/spec/fixtures/dbase_8b.dbt +0 -0
- data/spec/fixtures/dbase_8b_summary.txt +0 -15
- data/spec/fixtures/dbase_8c.dbf +0 -0
- data/spec/fixtures/dbase_f5.dbf +0 -0
- data/spec/fixtures/dbase_f5.fpt +0 -0
- data/spec/fixtures/dbase_f5_summary.txt +0 -68
- data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DBC +0 -0
- data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DCT +0 -0
- data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DCX +0 -0
- data/spec/fixtures/foxprodb/calls.CDX +0 -0
- data/spec/fixtures/foxprodb/calls.FPT +0 -0
- data/spec/fixtures/foxprodb/calls.dbf +0 -0
- data/spec/fixtures/foxprodb/contacts.CDX +0 -0
- data/spec/fixtures/foxprodb/contacts.FPT +0 -0
- data/spec/fixtures/foxprodb/contacts.dbf +0 -0
- data/spec/fixtures/foxprodb/setup.CDX +0 -0
- data/spec/fixtures/foxprodb/setup.dbf +0 -0
- data/spec/fixtures/foxprodb/types.CDX +0 -0
- data/spec/fixtures/foxprodb/types.dbf +0 -0
- data/spec/fixtures/polygon.dbf +0 -0
- data/spec/spec_helper.rb +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 96b3352f6c6d2cc3a667c6f390021c4f99be7c83ff2b9c911ac38c75d4c261aa
|
|
4
|
+
data.tar.gz: b787cd0e14a34ea66e670f88e793bcbebc1ccbdc260721da08ccee5d3de2624a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 52ceab2c71a3093ff691be23a0a0590e4309fd88218e683032e12805585dea05dd97bebd0170f003e7678641810d5d9aa6b9731bb0af647a8508d015875312d2
|
|
7
|
+
data.tar.gz: 41b87283de2deea377a3c27436d621486af57bc0421e5bc84dc1bd1f304abf8d264d72d8dda80350666895ca604efd335b829fd1efc79720721027146fe56b7d
|
data/CHANGELOG.md
CHANGED
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.
|
|
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)) #
|
|
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
|
-
|
|
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 = '
|
|
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.
|
|
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.
|
|
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 :
|
|
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::
|
|
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
|
-
|
|
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
|
-
# @
|
|
59
|
-
|
|
60
|
-
|
|
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
|
|
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.
|
|
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
|
data/lib/dbf/column_type.rb
CHANGED
|
@@ -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.
|
|
44
|
+
return nil if value.empty?
|
|
27
45
|
|
|
28
|
-
|
|
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
|
|
64
|
+
class AutoIncrement < Base
|
|
47
65
|
# @param value [String]
|
|
48
66
|
def type_cast(value)
|
|
49
|
-
|
|
50
|
-
sign_multiplier =
|
|
51
|
-
|
|
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.
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
|
118
|
-
|
|
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
|
data/lib/dbf/database/foxpro.rb
CHANGED
|
@@ -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: #{
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
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
|
|
90
|
+
{name:, fields: []}
|
|
109
91
|
end
|
|
110
92
|
end
|
|
111
93
|
|
|
112
94
|
class Table < DBF::Table
|
|
113
|
-
|
|
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
|
-
@
|
|
9
|
-
|
|
10
|
-
|
|
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 =
|
|
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 =
|
|
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
data/lib/dbf/memo/dbase3.rb
CHANGED
|
@@ -4,10 +4,10 @@ module DBF
|
|
|
4
4
|
module Memo
|
|
5
5
|
class Dbase3 < Base
|
|
6
6
|
def build_memo(start_block) # :nodoc:
|
|
7
|
-
|
|
7
|
+
data.seek offset(start_block)
|
|
8
8
|
memo_string = +''
|
|
9
9
|
loop do
|
|
10
|
-
block =
|
|
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
|
data/lib/dbf/memo/dbase4.rb
CHANGED
|
@@ -4,8 +4,8 @@ module DBF
|
|
|
4
4
|
module Memo
|
|
5
5
|
class Dbase4 < Base
|
|
6
6
|
def build_memo(start_block) # :nodoc:
|
|
7
|
-
|
|
8
|
-
|
|
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
|
data/lib/dbf/memo/foxpro.rb
CHANGED
|
@@ -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
|
-
|
|
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
|