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.
Files changed (81) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +19 -9
  4. data/bin/dbf +2 -54
  5. data/dbf.gemspec +6 -9
  6. data/lib/dbf/cli.rb +95 -0
  7. data/lib/dbf/column.rb +17 -15
  8. data/lib/dbf/column_builder.rb +31 -0
  9. data/lib/dbf/column_type.rb +66 -16
  10. data/lib/dbf/database/foxpro.rb +19 -32
  11. data/lib/dbf/encoder.rb +116 -0
  12. data/lib/dbf/encodings.rb +8 -0
  13. data/lib/dbf/file_handler.rb +36 -0
  14. data/lib/dbf/find.rb +54 -0
  15. data/lib/dbf/header.rb +5 -8
  16. data/lib/dbf/memo/base.rb +2 -0
  17. data/lib/dbf/memo/dbase3.rb +2 -2
  18. data/lib/dbf/memo/dbase4.rb +2 -2
  19. data/lib/dbf/memo/foxpro.rb +14 -7
  20. data/lib/dbf/record.rb +60 -34
  21. data/lib/dbf/record_context.rb +5 -0
  22. data/lib/dbf/record_iterator.rb +35 -0
  23. data/lib/dbf/schema.rb +21 -21
  24. data/lib/dbf/table.rb +50 -181
  25. data/lib/dbf/version.rb +1 -1
  26. data/lib/dbf/version_config.rb +79 -0
  27. data/lib/dbf.rb +8 -0
  28. metadata +17 -64
  29. data/spec/dbf/column_spec.rb +0 -287
  30. data/spec/dbf/database/foxpro_spec.rb +0 -53
  31. data/spec/dbf/encoding_spec.rb +0 -49
  32. data/spec/dbf/file_formats_spec.rb +0 -221
  33. data/spec/dbf/record_spec.rb +0 -116
  34. data/spec/dbf/table_spec.rb +0 -377
  35. data/spec/fixtures/cp1251.dbf +0 -0
  36. data/spec/fixtures/cp1251_summary.txt +0 -12
  37. data/spec/fixtures/dbase_02.dbf +0 -0
  38. data/spec/fixtures/dbase_02_summary.txt +0 -23
  39. data/spec/fixtures/dbase_03.dbf +0 -0
  40. data/spec/fixtures/dbase_03_cyrillic.dbf +0 -0
  41. data/spec/fixtures/dbase_03_cyrillic_summary.txt +0 -11
  42. data/spec/fixtures/dbase_03_summary.txt +0 -40
  43. data/spec/fixtures/dbase_30.dbf +0 -0
  44. data/spec/fixtures/dbase_30.fpt +0 -0
  45. data/spec/fixtures/dbase_30_summary.txt +0 -154
  46. data/spec/fixtures/dbase_31.dbf +0 -0
  47. data/spec/fixtures/dbase_31_summary.txt +0 -20
  48. data/spec/fixtures/dbase_32.dbf +0 -0
  49. data/spec/fixtures/dbase_32_summary.txt +0 -11
  50. data/spec/fixtures/dbase_83.dbf +0 -0
  51. data/spec/fixtures/dbase_83.dbt +0 -0
  52. data/spec/fixtures/dbase_83_missing_memo.dbf +0 -0
  53. data/spec/fixtures/dbase_83_missing_memo_record_0.yml +0 -16
  54. data/spec/fixtures/dbase_83_record_0.yml +0 -16
  55. data/spec/fixtures/dbase_83_record_9.yml +0 -23
  56. data/spec/fixtures/dbase_83_schema_ar.txt +0 -19
  57. data/spec/fixtures/dbase_83_schema_sq.txt +0 -21
  58. data/spec/fixtures/dbase_83_schema_sq_lim.txt +0 -21
  59. data/spec/fixtures/dbase_83_summary.txt +0 -24
  60. data/spec/fixtures/dbase_8b.dbf +0 -0
  61. data/spec/fixtures/dbase_8b.dbt +0 -0
  62. data/spec/fixtures/dbase_8b_summary.txt +0 -15
  63. data/spec/fixtures/dbase_8c.dbf +0 -0
  64. data/spec/fixtures/dbase_f5.dbf +0 -0
  65. data/spec/fixtures/dbase_f5.fpt +0 -0
  66. data/spec/fixtures/dbase_f5_summary.txt +0 -68
  67. data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DBC +0 -0
  68. data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DCT +0 -0
  69. data/spec/fixtures/foxprodb/FOXPRO-DB-TEST.DCX +0 -0
  70. data/spec/fixtures/foxprodb/calls.CDX +0 -0
  71. data/spec/fixtures/foxprodb/calls.FPT +0 -0
  72. data/spec/fixtures/foxprodb/calls.dbf +0 -0
  73. data/spec/fixtures/foxprodb/contacts.CDX +0 -0
  74. data/spec/fixtures/foxprodb/contacts.FPT +0 -0
  75. data/spec/fixtures/foxprodb/contacts.dbf +0 -0
  76. data/spec/fixtures/foxprodb/setup.CDX +0 -0
  77. data/spec/fixtures/foxprodb/setup.dbf +0 -0
  78. data/spec/fixtures/foxprodb/types.CDX +0 -0
  79. data/spec/fixtures/foxprodb/types.dbf +0 -0
  80. data/spec/fixtures/polygon.dbf +0 -0
  81. data/spec/spec_helper.rb +0 -35
@@ -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
- @data = data
9
- unpack_header
10
- end
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 = @data.unpack('x v x3 v')
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 = @data.unpack('x x3 V v2 x17 H2')
17
+ @record_count, @header_length, @record_length, @encoding_key = data.unpack('x x3 V v2 x17 H2')
21
18
  @encoding = DBF::ENCODINGS[@encoding_key]
22
19
  end
23
20
  end
data/lib/dbf/memo/base.rb CHANGED
@@ -31,6 +31,8 @@ module DBF
31
31
 
32
32
  private
33
33
 
34
+ attr_reader :data
35
+
34
36
  def offset(start_block) # :nodoc:
35
37
  start_block * block_size
36
38
  end
@@ -4,10 +4,10 @@ module DBF
4
4
  module Memo
5
5
  class Dbase3 < Base
6
6
  def build_memo(start_block) # :nodoc:
7
- @data.seek offset(start_block)
7
+ data.seek offset(start_block)
8
8
  memo_string = +''
9
9
  loop do
10
- block = @data.read(BLOCK_SIZE).gsub(/(\000|\032)/, '')
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
@@ -4,8 +4,8 @@ module DBF
4
4
  module Memo
5
5
  class Dbase4 < Base
6
6
  def build_memo(start_block) # :nodoc:
7
- @data.seek offset(start_block)
8
- @data.read(@data.read(BLOCK_HEADER_SIZE).unpack1('x4L'))
7
+ data.seek offset(start_block)
8
+ data.read(data.read(BLOCK_HEADER_SIZE).unpack1('x4L'))
9
9
  end
10
10
  end
11
11
  end
@@ -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
- if memo_size > block_content_size
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 columns [Column]
10
- # @param version [String]
11
- # @param memo [DBF::Memo]
12
- def initialize(data, columns, version, memo)
13
- @data = StringIO.new(data)
14
- @columns = columns
15
- @version = version
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
- other.respond_to?(:attributes) && other.attributes == attributes
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 attributes.key?(key)
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 ||= @columns.map { |column| init_attribute(column) }
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 column_names # :nodoc:
64
- @column_names ||= @columns.map(&:name)
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 get_data(column) # :nodoc:
68
- @data.read(column.length)
93
+ def column_name_index(key) # :nodoc:
94
+ column_names.index(key) || underscored_column_names.index(key)
69
95
  end
70
96
 
71
- def get_memo(column) # :nodoc:
72
- if @memo
73
- @memo.get(memo_start_block(column))
74
- else
75
- # the memo file is missing, so read ahead to next record and return nil
76
- @data.read(column.length)
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 init_attribute(column) # :nodoc:
82
- value = column.memo? ? get_memo(column) : get_data(column)
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 memo_start_block(column) # :nodoc:
87
- data = get_data(column)
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
- if (index = underscored_column_names.index(method.to_s))
94
- attributes[@columns[index].name]
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,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DBF
4
+ RecordContext = Struct.new(:columns, :version, :memo, :column_offsets, keyword_init: true)
5
+ 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
- s = +"ActiveRecord::Schema.define do\n"
53
- s << " create_table \"#{name}\" do |t|\n"
57
+ output = +"ActiveRecord::Schema.define do\n"
58
+ output << " create_table \"#{name}\" do |t|\n"
54
59
  columns.each do |column|
55
- s << " t.column #{activerecord_schema_definition(column)}"
60
+ output << " t.column #{activerecord_schema_definition(column)}"
56
61
  end
57
- s << " end\nend"
58
- s
62
+ output << " end\nend"
63
+ output
59
64
  end
60
65
 
61
66
  def sequel_schema(table_only: false) # :nodoc:
62
- s = +''
63
- s << "Sequel.migration do\n" unless table_only
64
- s << " change do\n " unless table_only
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
- s << " column #{sequel_schema_definition(column)}"
71
+ output << " column #{sequel_schema_definition(column)}"
68
72
  end
69
- s << " end\n"
70
- s << " end\n" unless table_only
71
- s << "end\n" unless table_only
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
- case column.type
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[column.type]
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
- if format == :sequel
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