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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +8 -0
- data/README.md +4 -2
- data/dbf.gemspec +6 -9
- data/lib/dbf/column.rb +19 -15
- data/lib/dbf/column_builder.rb +31 -0
- data/lib/dbf/column_type.rb +61 -15
- data/lib/dbf/database/foxpro.rb +21 -32
- data/lib/dbf/encodings.rb +2 -0
- data/lib/dbf/file_handler.rb +36 -0
- data/lib/dbf/find.rb +54 -0
- data/lib/dbf/header.rb +7 -8
- data/lib/dbf/memo/base.rb +4 -0
- data/lib/dbf/memo/dbase3.rb +5 -3
- data/lib/dbf/memo/dbase4.rb +4 -2
- data/lib/dbf/memo/foxpro.rb +16 -7
- data/lib/dbf/record.rb +62 -34
- data/lib/dbf/record_context.rb +5 -0
- data/lib/dbf/record_iterator.rb +35 -0
- data/lib/dbf/schema.rb +23 -21
- data/lib/dbf/table.rb +44 -178
- data/lib/dbf/version.rb +3 -1
- data/lib/dbf/version_config.rb +79 -0
- data/lib/dbf.rb +8 -0
- metadata +15 -64
- data/spec/dbf/column_spec.rb +0 -286
- data/spec/dbf/database/foxpro_spec.rb +0 -51
- data/spec/dbf/encoding_spec.rb +0 -47
- data/spec/dbf/file_formats_spec.rb +0 -219
- data/spec/dbf/record_spec.rb +0 -114
- data/spec/dbf/table_spec.rb +0 -375
- 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 -33
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
|
@@ -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 :
|
|
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::
|
|
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
|
-
|
|
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
|
-
# @
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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.
|
|
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
|
data/lib/dbf/column_type.rb
CHANGED
|
@@ -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.
|
|
44
|
+
return nil if value.empty?
|
|
25
45
|
|
|
26
|
-
|
|
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
|
|
64
|
+
class AutoIncrement < Base
|
|
45
65
|
# @param value [String]
|
|
46
66
|
def type_cast(value)
|
|
47
|
-
|
|
48
|
-
sign_multiplier =
|
|
49
|
-
|
|
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.
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
116
|
-
|
|
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
|
data/lib/dbf/database/foxpro.rb
CHANGED
|
@@ -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: #{
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
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
|
-
|
|
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
|
|
90
|
+
{name:, fields: []}
|
|
107
91
|
end
|
|
108
92
|
end
|
|
109
93
|
|
|
110
94
|
class Table < DBF::Table
|
|
111
|
-
|
|
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
|
@@ -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
|
-
@
|
|
7
|
-
|
|
8
|
-
|
|
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 =
|
|
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 =
|
|
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
data/lib/dbf/memo/dbase3.rb
CHANGED
|
@@ -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
|
-
|
|
6
|
-
memo_string = ''
|
|
7
|
+
data.seek offset(start_block)
|
|
8
|
+
memo_string = +''
|
|
7
9
|
loop do
|
|
8
|
-
block =
|
|
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
|
data/lib/dbf/memo/dbase4.rb
CHANGED
|
@@ -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
|
-
|
|
6
|
-
|
|
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
|
data/lib/dbf/memo/foxpro.rb
CHANGED
|
@@ -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
|
-
|
|
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
|