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
data/lib/dbf/encoder.rb
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBF
|
|
4
|
+
# Transcodes strings from a source encoding to a target encoding.
|
|
5
|
+
#
|
|
6
|
+
# Most DBF code pages map to encodings built into Ruby and are converted
|
|
7
|
+
# with String#encode. A few (Mazovia cp620, Kamenický cp895) have no Ruby
|
|
8
|
+
# equivalent and are decoded through vendored byte-to-Unicode tables.
|
|
9
|
+
module Encoder
|
|
10
|
+
# Byte-to-Unicode tables generated from
|
|
11
|
+
# https://github.com/SheetJS/js-codepage/blob/master/codepages/620.TBL and
|
|
12
|
+
# https://github.com/SheetJS/js-codepage/blob/master/codepages/895.TBL
|
|
13
|
+
|
|
14
|
+
# Mazovia (Polish) MS-DOS
|
|
15
|
+
CP620_TO_UNICODE = [
|
|
16
|
+
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
|
|
17
|
+
0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
|
18
|
+
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
|
|
19
|
+
0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
|
|
20
|
+
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
|
|
21
|
+
0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
|
|
22
|
+
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
|
23
|
+
0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
|
|
24
|
+
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
|
25
|
+
0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
|
|
26
|
+
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
|
|
27
|
+
0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
|
|
28
|
+
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
|
29
|
+
0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
|
|
30
|
+
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
|
|
31
|
+
0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F,
|
|
32
|
+
0x00C7, 0x00FC, 0x00E9, 0x00E2, 0x00E4, 0x00E0, 0x0105, 0x00E7,
|
|
33
|
+
0x00EA, 0x00EB, 0x00E8, 0x00EF, 0x00EE, 0x0107, 0x00C4, 0x0104,
|
|
34
|
+
0x0118, 0x0119, 0x0142, 0x00F4, 0x00F6, 0x0106, 0x00FB, 0x00F9,
|
|
35
|
+
0x015A, 0x00D6, 0x00DC, 0x00A2, 0x0141, 0x00A5, 0x015B, 0x0192,
|
|
36
|
+
0x0179, 0x017B, 0x00F3, 0x00D3, 0x0144, 0x0143, 0x017A, 0x017C,
|
|
37
|
+
0x00BF, 0x2310, 0x00AC, 0x00BD, 0x00BC, 0x00A1, 0x00AB, 0x00BB,
|
|
38
|
+
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
|
|
39
|
+
0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
|
|
40
|
+
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
|
41
|
+
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
|
|
42
|
+
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B,
|
|
43
|
+
0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
|
|
44
|
+
0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4,
|
|
45
|
+
0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229,
|
|
46
|
+
0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248,
|
|
47
|
+
0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0
|
|
48
|
+
].freeze
|
|
49
|
+
|
|
50
|
+
# Kamenický (Czech) MS-DOS
|
|
51
|
+
CP895_TO_UNICODE = [
|
|
52
|
+
0x0000, 0x0001, 0x0002, 0x0003, 0x0004, 0x0005, 0x0006, 0x0007,
|
|
53
|
+
0x0008, 0x0009, 0x000A, 0x000B, 0x000C, 0x000D, 0x000E, 0x000F,
|
|
54
|
+
0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015, 0x0016, 0x0017,
|
|
55
|
+
0x0018, 0x0019, 0x001A, 0x001B, 0x001C, 0x001D, 0x001E, 0x001F,
|
|
56
|
+
0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025, 0x0026, 0x0027,
|
|
57
|
+
0x0028, 0x0029, 0x002A, 0x002B, 0x002C, 0x002D, 0x002E, 0x002F,
|
|
58
|
+
0x0030, 0x0031, 0x0032, 0x0033, 0x0034, 0x0035, 0x0036, 0x0037,
|
|
59
|
+
0x0038, 0x0039, 0x003A, 0x003B, 0x003C, 0x003D, 0x003E, 0x003F,
|
|
60
|
+
0x0040, 0x0041, 0x0042, 0x0043, 0x0044, 0x0045, 0x0046, 0x0047,
|
|
61
|
+
0x0048, 0x0049, 0x004A, 0x004B, 0x004C, 0x004D, 0x004E, 0x004F,
|
|
62
|
+
0x0050, 0x0051, 0x0052, 0x0053, 0x0054, 0x0055, 0x0056, 0x0057,
|
|
63
|
+
0x0058, 0x0059, 0x005A, 0x005B, 0x005C, 0x005D, 0x005E, 0x005F,
|
|
64
|
+
0x0060, 0x0061, 0x0062, 0x0063, 0x0064, 0x0065, 0x0066, 0x0067,
|
|
65
|
+
0x0068, 0x0069, 0x006A, 0x006B, 0x006C, 0x006D, 0x006E, 0x006F,
|
|
66
|
+
0x0070, 0x0071, 0x0072, 0x0073, 0x0074, 0x0075, 0x0076, 0x0077,
|
|
67
|
+
0x0078, 0x0079, 0x007A, 0x007B, 0x007C, 0x007D, 0x007E, 0x007F,
|
|
68
|
+
0x010C, 0x00FC, 0x00E9, 0x010F, 0x00E4, 0x010E, 0x0164, 0x010D,
|
|
69
|
+
0x011B, 0x011A, 0x0139, 0x00CD, 0x013E, 0x01EA, 0x00C4, 0x00C1,
|
|
70
|
+
0x00C9, 0x017E, 0x017D, 0x00F4, 0x00F6, 0x00D3, 0x016F, 0x00DA,
|
|
71
|
+
0x00FD, 0x00D6, 0x00DC, 0x0160, 0x013D, 0x00DD, 0x0158, 0x0165,
|
|
72
|
+
0x00E1, 0x00ED, 0x00F3, 0x00FA, 0x0148, 0x0147, 0x016E, 0x00D4,
|
|
73
|
+
0x0161, 0x0159, 0x0155, 0x0154, 0x00BC, 0x00A7, 0x00AB, 0x00BB,
|
|
74
|
+
0x2591, 0x2592, 0x2593, 0x2502, 0x2524, 0x2561, 0x2562, 0x2556,
|
|
75
|
+
0x2555, 0x2563, 0x2551, 0x2557, 0x255D, 0x255C, 0x255B, 0x2510,
|
|
76
|
+
0x2514, 0x2534, 0x252C, 0x251C, 0x2500, 0x253C, 0x255E, 0x255F,
|
|
77
|
+
0x255A, 0x2554, 0x2569, 0x2566, 0x2560, 0x2550, 0x256C, 0x2567,
|
|
78
|
+
0x2568, 0x2564, 0x2565, 0x2559, 0x2558, 0x2552, 0x2553, 0x256B,
|
|
79
|
+
0x256A, 0x2518, 0x250C, 0x2588, 0x2584, 0x258C, 0x2590, 0x2580,
|
|
80
|
+
0x03B1, 0x00DF, 0x0393, 0x03C0, 0x03A3, 0x03C3, 0x00B5, 0x03C4,
|
|
81
|
+
0x03A6, 0x0398, 0x03A9, 0x03B4, 0x221E, 0x03C6, 0x03B5, 0x2229,
|
|
82
|
+
0x2261, 0x00B1, 0x2265, 0x2264, 0x2320, 0x2321, 0x00F7, 0x2248,
|
|
83
|
+
0x00B0, 0x2219, 0x00B7, 0x221A, 0x207F, 0x00B2, 0x25A0, 0x00A0
|
|
84
|
+
].freeze
|
|
85
|
+
|
|
86
|
+
CUSTOM_TABLES = {
|
|
87
|
+
'cp620' => CP620_TO_UNICODE,
|
|
88
|
+
'cp895' => CP895_TO_UNICODE
|
|
89
|
+
}.freeze
|
|
90
|
+
|
|
91
|
+
# Is the encoding a code page without a Ruby Encoding, decoded through
|
|
92
|
+
# a vendored table?
|
|
93
|
+
#
|
|
94
|
+
# @param encoding [String, Encoding, nil]
|
|
95
|
+
# @return [Boolean]
|
|
96
|
+
def self.custom?(encoding)
|
|
97
|
+
encoding.is_a?(::String) && CUSTOM_TABLES.key?(encoding.downcase)
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Transcode a string from source_encoding to target_encoding
|
|
101
|
+
#
|
|
102
|
+
# @param string [String]
|
|
103
|
+
# @param source_encoding [String, Encoding]
|
|
104
|
+
# @param target_encoding [Encoding]
|
|
105
|
+
# @return [String]
|
|
106
|
+
def self.encode(string, source_encoding, target_encoding = Encoding.default_external)
|
|
107
|
+
table = source_encoding.is_a?(::String) && CUSTOM_TABLES[source_encoding.downcase]
|
|
108
|
+
if table
|
|
109
|
+
decoded = string.each_byte.map { |byte| table[byte] }.pack('U*')
|
|
110
|
+
target_encoding == Encoding::UTF_8 ? decoded : decoded.encode(target_encoding, undef: :replace, invalid: :replace)
|
|
111
|
+
else
|
|
112
|
+
string.force_encoding(source_encoding).encode(target_encoding, undef: :replace, invalid: :replace)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
data/lib/dbf/encodings.rb
CHANGED
|
@@ -5,6 +5,7 @@ module DBF
|
|
|
5
5
|
'01' => 'cp437', # U.S. MS-DOS
|
|
6
6
|
'02' => 'cp850', # International MS-DOS
|
|
7
7
|
'03' => 'cp1252', # Windows ANSI
|
|
8
|
+
'04' => 'macRoman', # Standard Macintosh
|
|
8
9
|
'08' => 'cp865', # Danish OEM
|
|
9
10
|
'09' => 'cp437', # Dutch OEM
|
|
10
11
|
'0a' => 'cp850', # Dutch OEM*
|
|
@@ -45,6 +46,8 @@ module DBF
|
|
|
45
46
|
'65' => 'cp866', # Russian MS-DOS
|
|
46
47
|
'66' => 'cp865', # Nordic MS-DOS
|
|
47
48
|
'67' => 'cp861', # Icelandic MS-DOS
|
|
49
|
+
'68' => 'cp895', # Kamenicky (Czech) MS-DOS
|
|
50
|
+
'69' => 'cp620', # Mazovia (Polish) MS-DOS
|
|
48
51
|
'6a' => 'cp737', # Greek MS-DOS (437G)
|
|
49
52
|
'6b' => 'cp857', # Turkish MS-DOS
|
|
50
53
|
'6c' => 'cp863', # French-Canadian MS-DOS
|
|
@@ -53,9 +56,14 @@ module DBF
|
|
|
53
56
|
'7a' => 'cp936', # PRC GBK
|
|
54
57
|
'7b' => 'cp932', # Japanese Shift-JIS
|
|
55
58
|
'7c' => 'cp874', # Thai Windows/MS-DOS
|
|
59
|
+
'7d' => 'cp1255', # Hebrew Windows
|
|
60
|
+
'7e' => 'cp1256', # Arabic Windows
|
|
56
61
|
'86' => 'cp737', # Greek OEM
|
|
57
62
|
'87' => 'cp852', # Slovenian OEM
|
|
58
63
|
'88' => 'cp857', # Turkish OEM
|
|
64
|
+
'96' => 'macCyrillic', # Russian Macintosh
|
|
65
|
+
'97' => 'macCentEuro', # Macintosh EE
|
|
66
|
+
'98' => 'macGreek', # Greek Macintosh
|
|
59
67
|
'c8' => 'cp1250', # Eastern European Windows
|
|
60
68
|
'c9' => 'cp1251', # Russian Windows
|
|
61
69
|
'ca' => 'cp1254', # Turkish Windows
|
|
@@ -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
|
data/lib/dbf/record.rb
CHANGED
|
@@ -6,14 +6,13 @@ module DBF
|
|
|
6
6
|
# Initialize a new DBF::Record
|
|
7
7
|
#
|
|
8
8
|
# @param data [String, StringIO] data
|
|
9
|
-
# @param
|
|
10
|
-
# @param
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
@
|
|
14
|
-
@
|
|
15
|
-
@
|
|
16
|
-
@memo = memo
|
|
9
|
+
# @param context [DBF::RecordContext]
|
|
10
|
+
# @param offset [Integer]
|
|
11
|
+
def initialize(data, context, offset = 0)
|
|
12
|
+
@data = data
|
|
13
|
+
@context = context
|
|
14
|
+
@offset = offset
|
|
15
|
+
@to_a = nil
|
|
17
16
|
end
|
|
18
17
|
|
|
19
18
|
# Equality
|
|
@@ -21,7 +20,9 @@ module DBF
|
|
|
21
20
|
# @param [DBF::Record] other
|
|
22
21
|
# @return [Boolean]
|
|
23
22
|
def ==(other)
|
|
24
|
-
|
|
23
|
+
attributes == other.attributes
|
|
24
|
+
rescue NoMethodError
|
|
25
|
+
false
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
# Reads attributes by column name
|
|
@@ -29,10 +30,13 @@ module DBF
|
|
|
29
30
|
# @param name [String, Symbol] key
|
|
30
31
|
def [](name)
|
|
31
32
|
key = name.to_s
|
|
32
|
-
if
|
|
33
|
+
if @context.column_offsets && !@to_a
|
|
34
|
+
index = column_name_index(key)
|
|
35
|
+
index ? column_value(index) : nil
|
|
36
|
+
elsif attributes.key?(key)
|
|
33
37
|
attributes[key]
|
|
34
38
|
elsif (index = underscored_column_names.index(key))
|
|
35
|
-
attributes[@columns[index].name]
|
|
39
|
+
attributes[@context.columns[index].name]
|
|
36
40
|
end
|
|
37
41
|
end
|
|
38
42
|
|
|
@@ -55,43 +59,65 @@ module DBF
|
|
|
55
59
|
#
|
|
56
60
|
# @return [Array]
|
|
57
61
|
def to_a
|
|
58
|
-
@to_a ||=
|
|
62
|
+
@to_a ||= begin
|
|
63
|
+
data = @data
|
|
64
|
+
offset = @offset
|
|
65
|
+
columns = @context.columns
|
|
66
|
+
col_count = columns.length
|
|
67
|
+
result = Array.new(col_count)
|
|
68
|
+
index = 0
|
|
69
|
+
while index < col_count
|
|
70
|
+
column = columns[index]
|
|
71
|
+
len = column.length
|
|
72
|
+
raw = data.byteslice(offset, len)
|
|
73
|
+
offset += len
|
|
74
|
+
result[index] = decode_column(raw, column)
|
|
75
|
+
index += 1
|
|
76
|
+
end
|
|
77
|
+
@offset = offset
|
|
78
|
+
result
|
|
79
|
+
end
|
|
59
80
|
end
|
|
60
81
|
|
|
61
82
|
private
|
|
62
83
|
|
|
63
|
-
def
|
|
64
|
-
|
|
84
|
+
def decode_memo_value(raw) # :nodoc:
|
|
85
|
+
memo = @context.memo
|
|
86
|
+
return nil unless memo
|
|
87
|
+
|
|
88
|
+
version = @context.version
|
|
89
|
+
raw = raw.unpack1('V') if version == '30' || version == '31'
|
|
90
|
+
memo.get(raw.to_i)
|
|
65
91
|
end
|
|
66
92
|
|
|
67
|
-
def
|
|
68
|
-
|
|
93
|
+
def column_name_index(key) # :nodoc:
|
|
94
|
+
column_names.index(key) || underscored_column_names.index(key)
|
|
69
95
|
end
|
|
70
96
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
nil
|
|
78
|
-
end
|
|
97
|
+
def column_value(index) # :nodoc:
|
|
98
|
+
column = @context.columns[index]
|
|
99
|
+
col_offset = @offset + @context.column_offsets[index]
|
|
100
|
+
len = column.length
|
|
101
|
+
raw = @data.byteslice(col_offset, len)
|
|
102
|
+
decode_column(raw, column)
|
|
79
103
|
end
|
|
80
104
|
|
|
81
|
-
def
|
|
82
|
-
|
|
83
|
-
column.type_cast(value)
|
|
105
|
+
def decode_column(raw, column) # :nodoc:
|
|
106
|
+
column.decode(raw) { |raw_memo| decode_memo_value(raw_memo) }
|
|
84
107
|
end
|
|
85
108
|
|
|
86
|
-
def
|
|
87
|
-
|
|
88
|
-
data = data.unpack1('V') if %w[30 31].include?(@version)
|
|
89
|
-
data.to_i
|
|
109
|
+
def column_names # :nodoc:
|
|
110
|
+
@column_names ||= @context.columns.map(&:name)
|
|
90
111
|
end
|
|
91
112
|
|
|
92
113
|
def method_missing(method, *args) # :nodoc:
|
|
93
|
-
|
|
94
|
-
|
|
114
|
+
key = method.to_s
|
|
115
|
+
if (index = underscored_column_names.index(key))
|
|
116
|
+
if @context.column_offsets && !@to_a
|
|
117
|
+
column_value(index)
|
|
118
|
+
else
|
|
119
|
+
attributes[@context.columns[index].name]
|
|
120
|
+
end
|
|
95
121
|
else
|
|
96
122
|
super
|
|
97
123
|
end
|
|
@@ -102,7 +128,7 @@ module DBF
|
|
|
102
128
|
end
|
|
103
129
|
|
|
104
130
|
def underscored_column_names # :nodoc:
|
|
105
|
-
@underscored_column_names ||= @columns.map(&:underscored_name)
|
|
131
|
+
@underscored_column_names ||= @context.columns.map(&:underscored_name)
|
|
106
132
|
end
|
|
107
133
|
end
|
|
108
134
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module DBF
|
|
4
|
+
class RecordIterator
|
|
5
|
+
def initialize(data, context, header_length, record_length, record_count)
|
|
6
|
+
@data = data
|
|
7
|
+
@context = context
|
|
8
|
+
@header_length = header_length
|
|
9
|
+
@record_length = record_length
|
|
10
|
+
@record_count = record_count
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def each
|
|
14
|
+
buf = read_buffer
|
|
15
|
+
return unless buf
|
|
16
|
+
|
|
17
|
+
pos = 0
|
|
18
|
+
@record_count.times do
|
|
19
|
+
if buf.getbyte(pos) == 0x2A
|
|
20
|
+
yield nil
|
|
21
|
+
else
|
|
22
|
+
yield Record.new(buf, @context, pos + 1)
|
|
23
|
+
end
|
|
24
|
+
pos += @record_length
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def read_buffer
|
|
31
|
+
@data.seek(@header_length)
|
|
32
|
+
@data.read(@record_length * @record_count)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
data/lib/dbf/schema.rb
CHANGED
|
@@ -14,6 +14,11 @@ module DBF
|
|
|
14
14
|
'B' => ':binary'
|
|
15
15
|
}.freeze
|
|
16
16
|
|
|
17
|
+
STRING_DATA_FORMATS = {
|
|
18
|
+
sequel: ':varchar, :size => %s',
|
|
19
|
+
activerecord: ':string, :limit => %s'
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
17
22
|
# Generate an ActiveRecord::Schema
|
|
18
23
|
#
|
|
19
24
|
# xBase data types are converted to generic types as follows:
|
|
@@ -49,27 +54,25 @@ module DBF
|
|
|
49
54
|
end
|
|
50
55
|
|
|
51
56
|
def activerecord_schema(*) # :nodoc:
|
|
52
|
-
|
|
53
|
-
|
|
57
|
+
output = +"ActiveRecord::Schema.define do\n"
|
|
58
|
+
output << " create_table \"#{name}\" do |t|\n"
|
|
54
59
|
columns.each do |column|
|
|
55
|
-
|
|
60
|
+
output << " t.column #{activerecord_schema_definition(column)}"
|
|
56
61
|
end
|
|
57
|
-
|
|
58
|
-
|
|
62
|
+
output << " end\nend"
|
|
63
|
+
output
|
|
59
64
|
end
|
|
60
65
|
|
|
61
66
|
def sequel_schema(table_only: false) # :nodoc:
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
s << " create_table(:#{name}) do\n"
|
|
67
|
+
output = +''
|
|
68
|
+
output << "Sequel.migration do\n change do\n " unless table_only
|
|
69
|
+
output << " create_table(:#{name}) do\n"
|
|
66
70
|
columns.each do |column|
|
|
67
|
-
|
|
71
|
+
output << " column #{sequel_schema_definition(column)}"
|
|
68
72
|
end
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
s
|
|
73
|
+
output << " end\n"
|
|
74
|
+
output << " end\nend\n" unless table_only
|
|
75
|
+
output
|
|
73
76
|
end
|
|
74
77
|
|
|
75
78
|
def json_schema(*) # :nodoc:
|
|
@@ -93,11 +96,12 @@ module DBF
|
|
|
93
96
|
end
|
|
94
97
|
|
|
95
98
|
def schema_data_type(column, format = :activerecord) # :nodoc:
|
|
96
|
-
|
|
99
|
+
col_type = column.type
|
|
100
|
+
case col_type
|
|
97
101
|
when 'N', 'F', 'I'
|
|
98
102
|
number_data_type(column)
|
|
99
103
|
when 'Y', 'D', 'T', 'L', 'M', 'B'
|
|
100
|
-
OTHER_DATA_TYPES[
|
|
104
|
+
OTHER_DATA_TYPES[col_type]
|
|
101
105
|
else
|
|
102
106
|
string_data_format(format, column)
|
|
103
107
|
end
|
|
@@ -108,11 +112,7 @@ module DBF
|
|
|
108
112
|
end
|
|
109
113
|
|
|
110
114
|
def string_data_format(format, column)
|
|
111
|
-
|
|
112
|
-
":varchar, :size => #{column.length}"
|
|
113
|
-
else
|
|
114
|
-
":string, :limit => #{column.length}"
|
|
115
|
-
end
|
|
115
|
+
STRING_DATA_FORMATS.fetch(format, STRING_DATA_FORMATS[:activerecord]) % column.length
|
|
116
116
|
end
|
|
117
117
|
end
|
|
118
118
|
end
|