dbf 5.1.1 → 5.3.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 +13 -0
- data/README.md +19 -9
- data/bin/dbf +2 -54
- data/dbf.gemspec +6 -9
- data/lib/dbf/cli.rb +95 -0
- data/lib/dbf/column.rb +17 -15
- data/lib/dbf/column_builder.rb +31 -0
- data/lib/dbf/column_type.rb +66 -16
- data/lib/dbf/database/foxpro.rb +19 -32
- data/lib/dbf/encoder.rb +116 -0
- data/lib/dbf/encodings.rb +8 -0
- 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 +50 -181
- data/lib/dbf/version.rb +1 -1
- data/lib/dbf/version_config.rb +79 -0
- data/lib/dbf.rb +8 -0
- metadata +17 -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: d2d1ca393ae41262fd052c74ffe0ec497a963a2c036797686061312a0c492888
|
|
4
|
+
data.tar.gz: 7e92c62ace7713b883b6b2e925c34f7a8114ebd6345b36a81f97fb47109711e2
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0f3b1b02ab53f505ab3efc1dff89d2ad1169698ef7d9443b3eec3b758b921ae2df51bf443d4e877ac072bdef87c547607d1d39d913a5c361b4c31e21fee4b07
|
|
7
|
+
data.tar.gz: 6d826dd35aa6e80b671aeb804dc9991776851bbb9ca0ff5fb9ed6cd7139cbd40b0bad68327fc8f17b03b9ba2f2b77479331b7ad998c53e7ee32e5caee59241e6
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## main branch
|
|
4
|
+
|
|
5
|
+
- Add support for 8 more code pages (issue #98): Mazovia cp620 and Kamenický cp895 via vendored translation tables (new DBF::Encoder), plus macRoman, cp1255, cp1256, macCyrillic, macCentEuro and macGreek
|
|
6
|
+
- Blank Visual FoxPro "T" (DateTime) columns now return nil instead of a Julian day-0 date
|
|
7
|
+
- Blank "F" (Float) columns now return nil instead of 0.0, matching "N" (Number) behavior
|
|
8
|
+
- Extract CLI logic into DBF::CLI class (bin/dbf is now a thin shim)
|
|
9
|
+
- DBF::Table#to_csv now accepts an IO in addition to a String path
|
|
10
|
+
- Fix broken link in README
|
|
11
|
+
|
|
12
|
+
## 5.2.0
|
|
13
|
+
|
|
14
|
+
- Drop support for Ruby 3.1 and 3.2
|
|
15
|
+
|
|
3
16
|
## 5.1.1
|
|
4
17
|
|
|
5
18
|
- Frozen string literals
|
data/README.md
CHANGED
|
@@ -10,16 +10,18 @@
|
|
|
10
10
|
DBF is a small, fast Ruby library for reading dBase, xBase, Clipper, and FoxPro database files.
|
|
11
11
|
|
|
12
12
|
* Project page: <https://github.com/infused/dbf>
|
|
13
|
-
* API Documentation: <https://
|
|
13
|
+
* API Documentation: <https://rdoc.info/gems/dbf>
|
|
14
14
|
* Report bugs: <https://github.com/infused/dbf/issues>
|
|
15
15
|
* Questions: Email <mailto:keithm@infused.org> and put DBF somewhere in the
|
|
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,
|
|
22
|
-
please use 3.0.x (<https://github.com/infused
|
|
24
|
+
please use 3.0.x (<https://github.com/infused/dbf/tree/3_stable>)
|
|
23
25
|
|
|
24
26
|
NOTE: Beginning with version 3 we have dropped support for Ruby 1.8 and 1.9. If you need support for older Rubies,
|
|
25
27
|
please use 2.0.x (<https://github.com/infused/dbf/tree/2_stable>)
|
|
@@ -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
|
|
|
@@ -149,6 +151,7 @@ table = DBF::Table.new('dbf/books.dbf', nil, 'cp866')
|
|
|
149
151
|
| 01 | cp437 | U.S. MS–DOS |
|
|
150
152
|
| 02 | cp850 | International MS–DOS |
|
|
151
153
|
| 03 | cp1252 | Windows ANSI |
|
|
154
|
+
| 04 | macRoman | Standard Macintosh |
|
|
152
155
|
| 08 | cp865 | Danish OEM |
|
|
153
156
|
| 09 | cp437 | Dutch OEM |
|
|
154
157
|
| 0a | cp850 | Dutch OEM* |
|
|
@@ -189,6 +192,8 @@ table = DBF::Table.new('dbf/books.dbf', nil, 'cp866')
|
|
|
189
192
|
| 65 | cp866 | Russian MS–DOS |
|
|
190
193
|
| 66 | cp865 | Nordic MS–DOS |
|
|
191
194
|
| 67 | cp861 | Icelandic MS–DOS |
|
|
195
|
+
| 68 | cp895 | Kamenický (Czech) MS–DOS |
|
|
196
|
+
| 69 | cp620 | Mazovia (Polish) MS–DOS |
|
|
192
197
|
| 6a | cp737 | Greek MS–DOS (437G) |
|
|
193
198
|
| 6b | cp857 | Turkish MS–DOS |
|
|
194
199
|
| 6c | cp863 | French–Canadian MS–DOS |
|
|
@@ -197,9 +202,14 @@ table = DBF::Table.new('dbf/books.dbf', nil, 'cp866')
|
|
|
197
202
|
| 7a | cp936 | PRC GBK |
|
|
198
203
|
| 7b | cp932 | Japanese Shift-JIS |
|
|
199
204
|
| 7c | cp874 | Thai Windows/MS–DOS |
|
|
205
|
+
| 7d | cp1255 | Hebrew Windows |
|
|
206
|
+
| 7e | cp1256 | Arabic Windows |
|
|
200
207
|
| 86 | cp737 | Greek OEM |
|
|
201
208
|
| 87 | cp852 | Slovenian OEM |
|
|
202
209
|
| 88 | cp857 | Turkish OEM |
|
|
210
|
+
| 96 | macCyrillic | Russian Macintosh |
|
|
211
|
+
| 97 | macCentEuro | Macintosh EE |
|
|
212
|
+
| 98 | macGreek | Greek Macintosh |
|
|
203
213
|
| c8 | cp1250 | Eastern European Windows |
|
|
204
214
|
| c9 | cp1251 | Russian Windows |
|
|
205
215
|
| ca | cp1254 | Turkish Windows |
|
|
@@ -251,7 +261,7 @@ class Book < Sequel::Model; end
|
|
|
251
261
|
Sequel.migration do
|
|
252
262
|
up do
|
|
253
263
|
table = DBF::Table.new('db/dbf/books.dbf')
|
|
254
|
-
eval(table.schema(:sequel, true)) #
|
|
264
|
+
eval(table.schema(:sequel, table_only: true)) # limit output to create_table() only
|
|
255
265
|
|
|
256
266
|
Book.reset_column_information
|
|
257
267
|
table.each do |record|
|
|
@@ -326,16 +336,16 @@ at this time.
|
|
|
326
336
|
| 04 | dBase IV without memo file | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
327
337
|
| 05 | dBase V without memo file | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
328
338
|
| 07 | Visual Objects 1.x | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
329
|
-
| 30 | Visual FoxPro | Y | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
|
330
|
-
| 31 | Visual FoxPro with AutoIncrement | Y | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
|
331
|
-
| 32 | Visual FoxPro with field type Varchar or Varbinary | Y | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
|
339
|
+
| 30 | Visual FoxPro | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | Y | N | N | N | N | - |
|
|
340
|
+
| 31 | Visual FoxPro with AutoIncrement | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | Y | N | N | N | N | N |
|
|
341
|
+
| 32 | Visual FoxPro with field type Varchar or Varbinary | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | Y | N | N | N | N | N |
|
|
332
342
|
| 7b | dBase IV with memo file | Y | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - |
|
|
333
343
|
| 83 | dBase III with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
334
344
|
| 87 | Visual Objects 1.x with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - |
|
|
335
345
|
| 8b | dBase IV with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | N | - | - | - |
|
|
336
346
|
| 8e | dBase IV with SQL table | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | N | - | - | - |
|
|
337
|
-
| f5 | FoxPro with memo file | Y | Y | Y | Y | Y | Y | Y | Y | N | Y |
|
|
338
|
-
| fb | FoxPro without memo file | Y | Y | Y | Y | - | Y | Y | Y | N | Y |
|
|
347
|
+
| f5 | FoxPro with memo file | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | Y | Y | N | N | N | N | N |
|
|
348
|
+
| fb | FoxPro without memo file | Y | Y | Y | Y | - | Y | Y | Y | N | Y | Y | Y | N | N | N | N | N |
|
|
339
349
|
|
|
340
350
|
Data type descriptions
|
|
341
351
|
|
data/bin/dbf
CHANGED
|
@@ -1,60 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
2
3
|
|
|
3
4
|
Signal.trap('PIPE', 'SYSTEM_DEFAULT')
|
|
4
5
|
|
|
5
6
|
require 'dbf'
|
|
6
|
-
require 'dbf/version'
|
|
7
|
-
require 'optparse'
|
|
8
7
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
if params['v']
|
|
12
|
-
puts "dbf version: #{DBF::VERSION}"
|
|
13
|
-
|
|
14
|
-
elsif params['h']
|
|
15
|
-
puts "usage: #{File.basename(__FILE__)} [-h|-s|-a|-c|-r] filename"
|
|
16
|
-
puts ' -h = print this message'
|
|
17
|
-
puts ' -v = print the DBF gem version'
|
|
18
|
-
puts ' -s = print summary information'
|
|
19
|
-
puts ' -a = create an ActiveRecord::Schema'
|
|
20
|
-
puts ' -r = create a Sequel migration'
|
|
21
|
-
puts ' -c = export as CSV'
|
|
22
|
-
else
|
|
23
|
-
|
|
24
|
-
filename = ARGV.shift
|
|
25
|
-
abort 'You must supply a filename on the command line' unless filename
|
|
26
|
-
|
|
27
|
-
# create an ActiveRecord::Schema
|
|
28
|
-
if params['a']
|
|
29
|
-
table = DBF::Table.new filename
|
|
30
|
-
puts table.schema(:activerecord)
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
# create an Sequel::Migration
|
|
34
|
-
if params['r']
|
|
35
|
-
table = DBF::Table.new filename
|
|
36
|
-
puts table.schema(:sequel)
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
if params['s']
|
|
40
|
-
table = DBF::Table.new filename
|
|
41
|
-
puts
|
|
42
|
-
puts "Database: #{filename}"
|
|
43
|
-
puts "Type: (#{table.version}) #{table.version_description}"
|
|
44
|
-
puts "Encoding: #{table.header_encoding}" if table.header_encoding
|
|
45
|
-
puts "Memo File: #{table.has_memo_file? ? 'true' : 'false'}"
|
|
46
|
-
puts "Records: #{table.record_count}"
|
|
47
|
-
|
|
48
|
-
puts "\nFields:"
|
|
49
|
-
puts 'Name Type Length Decimal'
|
|
50
|
-
puts '-' * 78
|
|
51
|
-
table.columns.each do |f|
|
|
52
|
-
puts format('%-16s %-10s %-10s %-10s', f.name, f.type, f.length, f.decimal)
|
|
53
|
-
end
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
if params['c']
|
|
57
|
-
table = DBF::Table.new filename
|
|
58
|
-
table.to_csv
|
|
59
|
-
end
|
|
60
|
-
end
|
|
8
|
+
exit DBF::CLI.run(ARGV)
|
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/cli.rb
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
|
|
5
|
+
module DBF
|
|
6
|
+
class CLI
|
|
7
|
+
USAGE = <<~HELP
|
|
8
|
+
usage: dbf [-h|-s|-a|-c|-r] filename
|
|
9
|
+
-h = print this message
|
|
10
|
+
-v = print the DBF gem version
|
|
11
|
+
-s = print summary information
|
|
12
|
+
-a = create an ActiveRecord::Schema
|
|
13
|
+
-r = create a Sequel migration
|
|
14
|
+
-c = export as CSV
|
|
15
|
+
HELP
|
|
16
|
+
|
|
17
|
+
def self.run(argv, stdout: $stdout, stderr: $stderr)
|
|
18
|
+
new(argv, stdout: stdout, stderr: stderr).run
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(argv, stdout: $stdout, stderr: $stderr)
|
|
22
|
+
@argv = argv.dup
|
|
23
|
+
@stdout = stdout
|
|
24
|
+
@stderr = stderr
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def run
|
|
28
|
+
params = OptionParser.new.getopts(@argv, 'h', 's', 'a', 'c', 'r', 'v')
|
|
29
|
+
|
|
30
|
+
if params['v']
|
|
31
|
+
print_version
|
|
32
|
+
elsif params['h']
|
|
33
|
+
print_help
|
|
34
|
+
else
|
|
35
|
+
filename = @argv.shift
|
|
36
|
+
return missing_filename unless filename
|
|
37
|
+
|
|
38
|
+
action = %w[a r s c].find { |flag| params[flag] }
|
|
39
|
+
case action
|
|
40
|
+
when 'a' then print_ar_schema(filename)
|
|
41
|
+
when 'r' then print_sequel_schema(filename)
|
|
42
|
+
when 's' then print_summary(filename)
|
|
43
|
+
when 'c' then print_csv(filename)
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
0
|
|
47
|
+
rescue DBF::FileNotFoundError => e
|
|
48
|
+
@stderr.puts "DBF::FileNotFoundError: #{e.message}"
|
|
49
|
+
1
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
def print_version
|
|
55
|
+
@stdout.puts "dbf version: #{DBF::VERSION}"
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def print_help
|
|
59
|
+
@stdout.puts USAGE
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def missing_filename
|
|
63
|
+
@stderr.puts 'You must supply a filename on the command line'
|
|
64
|
+
1
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def print_ar_schema(filename)
|
|
68
|
+
@stdout.puts DBF::Table.new(filename).schema(:activerecord)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def print_sequel_schema(filename)
|
|
72
|
+
@stdout.puts DBF::Table.new(filename).schema(:sequel)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def print_summary(filename)
|
|
76
|
+
table = DBF::Table.new(filename)
|
|
77
|
+
@stdout.puts
|
|
78
|
+
@stdout.puts "Database: #{filename}"
|
|
79
|
+
@stdout.puts "Type: (#{table.version}) #{table.version_description}"
|
|
80
|
+
@stdout.puts "Encoding: #{table.header_encoding}" if table.header_encoding
|
|
81
|
+
@stdout.puts "Memo File: #{table.has_memo_file? ? 'true' : 'false'}"
|
|
82
|
+
@stdout.puts "Records: #{table.record_count}"
|
|
83
|
+
@stdout.puts "\nFields:"
|
|
84
|
+
@stdout.puts 'Name Type Length Decimal'
|
|
85
|
+
@stdout.puts '-' * 78
|
|
86
|
+
table.columns.each do |f|
|
|
87
|
+
@stdout.puts format('%-16s %-10s %-10s %-10s', f.name, f.type, f.length, f.decimal)
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def print_csv(filename)
|
|
92
|
+
DBF::Table.new(filename).to_csv(@stdout)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
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, &)
|
|
62
|
+
type_cast_class.decode(raw, &)
|
|
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
|
@@ -5,12 +5,27 @@ module DBF
|
|
|
5
5
|
class Base
|
|
6
6
|
attr_reader :decimal, :encoding
|
|
7
7
|
|
|
8
|
-
# @param
|
|
9
|
-
# @param encoding [String, Encoding]
|
|
8
|
+
# @param column [DBF::Column]
|
|
10
9
|
def initialize(column)
|
|
11
10
|
@decimal = column.decimal
|
|
12
11
|
@encoding = column.encoding
|
|
13
12
|
end
|
|
13
|
+
|
|
14
|
+
def blank_value
|
|
15
|
+
nil
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def skip_blank?
|
|
19
|
+
false
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def decode(raw, &)
|
|
23
|
+
if skip_blank? && raw.count(' ') == raw.length
|
|
24
|
+
blank_value
|
|
25
|
+
else
|
|
26
|
+
type_cast(raw)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
14
29
|
end
|
|
15
30
|
|
|
16
31
|
class Nil < Base
|
|
@@ -21,11 +36,13 @@ module DBF
|
|
|
21
36
|
end
|
|
22
37
|
|
|
23
38
|
class Number < Base
|
|
39
|
+
def skip_blank? = true
|
|
40
|
+
|
|
24
41
|
# @param value [String]
|
|
25
42
|
def type_cast(value)
|
|
26
|
-
return nil if value.
|
|
43
|
+
return nil if value.empty?
|
|
27
44
|
|
|
28
|
-
|
|
45
|
+
decimal.zero? ? value.to_i : value.to_f
|
|
29
46
|
end
|
|
30
47
|
end
|
|
31
48
|
|
|
@@ -43,18 +60,22 @@ module DBF
|
|
|
43
60
|
end
|
|
44
61
|
end
|
|
45
62
|
|
|
46
|
-
class
|
|
63
|
+
class AutoIncrement < Base
|
|
47
64
|
# @param value [String]
|
|
48
65
|
def type_cast(value)
|
|
49
|
-
|
|
50
|
-
sign_multiplier =
|
|
51
|
-
|
|
66
|
+
bits = value.unpack1('B*')
|
|
67
|
+
sign_multiplier = bits[0] == '0' ? -1 : 1
|
|
68
|
+
bits[1, 31].to_i(2) * sign_multiplier
|
|
52
69
|
end
|
|
53
70
|
end
|
|
54
71
|
|
|
55
72
|
class Float < Base
|
|
73
|
+
def skip_blank? = true
|
|
74
|
+
|
|
56
75
|
# @param value [String]
|
|
57
76
|
def type_cast(value)
|
|
77
|
+
return nil if value.empty?
|
|
78
|
+
|
|
58
79
|
value.to_f
|
|
59
80
|
end
|
|
60
81
|
end
|
|
@@ -67,13 +88,20 @@ module DBF
|
|
|
67
88
|
end
|
|
68
89
|
|
|
69
90
|
class Boolean < Base
|
|
91
|
+
def skip_blank? = true
|
|
92
|
+
def blank_value = false
|
|
93
|
+
|
|
70
94
|
# @param value [String]
|
|
71
95
|
def type_cast(value)
|
|
72
|
-
value.
|
|
96
|
+
byte = value.getbyte(0)
|
|
97
|
+
byte == 89 || byte == 121 || byte == 84 || byte == 116 # Y y T t
|
|
73
98
|
end
|
|
74
99
|
end
|
|
75
100
|
|
|
76
101
|
class Date < Base
|
|
102
|
+
def skip_blank? = true
|
|
103
|
+
def blank_value = false
|
|
104
|
+
|
|
77
105
|
# @param value [String]
|
|
78
106
|
def type_cast(value)
|
|
79
107
|
value.match?(/\d{8}/) && ::Date.strptime(value, '%Y%m%d')
|
|
@@ -86,6 +114,8 @@ module DBF
|
|
|
86
114
|
# @param value [String]
|
|
87
115
|
def type_cast(value)
|
|
88
116
|
days, msecs = value.unpack('l2')
|
|
117
|
+
return nil if days.nil? || days.zero?
|
|
118
|
+
|
|
89
119
|
secs = (msecs / 1000).to_i
|
|
90
120
|
::DateTime.jd(days, (secs / 3600).to_i, (secs / 60).to_i % 60, secs % 60).to_time
|
|
91
121
|
rescue StandardError
|
|
@@ -94,13 +124,16 @@ module DBF
|
|
|
94
124
|
end
|
|
95
125
|
|
|
96
126
|
class Memo < Base
|
|
127
|
+
def decode(raw, &)
|
|
128
|
+
memo_content = yield(raw)
|
|
129
|
+
memo_content ? type_cast(memo_content) : nil
|
|
130
|
+
end
|
|
131
|
+
|
|
97
132
|
# @param value [String]
|
|
98
133
|
def type_cast(value)
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
value
|
|
103
|
-
end
|
|
134
|
+
return value unless encoding && value
|
|
135
|
+
|
|
136
|
+
Encoder.encode(value.dup, encoding)
|
|
104
137
|
end
|
|
105
138
|
end
|
|
106
139
|
|
|
@@ -112,10 +145,27 @@ module DBF
|
|
|
112
145
|
end
|
|
113
146
|
|
|
114
147
|
class String < Base
|
|
148
|
+
def initialize(column)
|
|
149
|
+
super
|
|
150
|
+
@target_encoding = Encoding.default_external
|
|
151
|
+
@needs_encode = encoding && encoding != @target_encoding
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def skip_blank? = true
|
|
155
|
+
def blank_value = ''
|
|
156
|
+
|
|
115
157
|
# @param value [String]
|
|
116
158
|
def type_cast(value)
|
|
117
|
-
value
|
|
118
|
-
|
|
159
|
+
value.strip!
|
|
160
|
+
encoding ? encode(value) : value
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
private
|
|
164
|
+
|
|
165
|
+
def encode(value)
|
|
166
|
+
return value.force_encoding(encoding) unless @needs_encode
|
|
167
|
+
|
|
168
|
+
Encoder.encode(value, encoding, @target_encoding)
|
|
119
169
|
end
|
|
120
170
|
end
|
|
121
171
|
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
|