dbf 5.2.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 96b3352f6c6d2cc3a667c6f390021c4f99be7c83ff2b9c911ac38c75d4c261aa
4
- data.tar.gz: b787cd0e14a34ea66e670f88e793bcbebc1ccbdc260721da08ccee5d3de2624a
3
+ metadata.gz: d2d1ca393ae41262fd052c74ffe0ec497a963a2c036797686061312a0c492888
4
+ data.tar.gz: 7e92c62ace7713b883b6b2e925c34f7a8114ebd6345b36a81f97fb47109711e2
5
5
  SHA512:
6
- metadata.gz: 52ceab2c71a3093ff691be23a0a0590e4309fd88218e683032e12805585dea05dd97bebd0170f003e7678641810d5d9aa6b9731bb0af647a8508d015875312d2
7
- data.tar.gz: 41b87283de2deea377a3c27436d621486af57bc0421e5bc84dc1bd1f304abf8d264d72d8dda80350666895ca604efd335b829fd1efc79720721027146fe56b7d
6
+ metadata.gz: f0f3b1b02ab53f505ab3efc1dff89d2ad1169698ef7d9443b3eec3b758b921ae2df51bf443d4e877ac072bdef87c547607d1d39d913a5c361b4c31e21fee4b07
7
+ data.tar.gz: 6d826dd35aa6e80b671aeb804dc9991776851bbb9ca0ff5fb9ed6cd7139cbd40b0bad68327fc8f17b03b9ba2f2b77479331b7ad998c53e7ee32e5caee59241e6
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
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
+
3
12
  ## 5.2.0
4
13
 
5
14
  - Drop support for Ruby 3.1 and 3.2
data/README.md CHANGED
@@ -10,7 +10,7 @@
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://rubydoc.info/github/infused/dbf>
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
@@ -21,7 +21,7 @@ NOTE: Beginning with version 5.2 we have dropped support for Ruby 3.2 and earlie
21
21
  NOTE: Beginning with version 4.3 we have dropped support for Ruby 3.0 and earlier.
22
22
 
23
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,
24
- please use 3.0.x (<https://github.com/infused.org/dbf/tree/3_stable>)
24
+ please use 3.0.x (<https://github.com/infused/dbf/tree/3_stable>)
25
25
 
26
26
  NOTE: Beginning with version 3 we have dropped support for Ruby 1.8 and 1.9. If you need support for older Rubies,
27
27
  please use 2.0.x (<https://github.com/infused/dbf/tree/2_stable>)
@@ -151,6 +151,7 @@ table = DBF::Table.new('dbf/books.dbf', nil, 'cp866')
151
151
  | 01 | cp437 | U.S. MS–DOS |
152
152
  | 02 | cp850 | International MS–DOS |
153
153
  | 03 | cp1252 | Windows ANSI |
154
+ | 04 | macRoman | Standard Macintosh |
154
155
  | 08 | cp865 | Danish OEM |
155
156
  | 09 | cp437 | Dutch OEM |
156
157
  | 0a | cp850 | Dutch OEM* |
@@ -191,6 +192,8 @@ table = DBF::Table.new('dbf/books.dbf', nil, 'cp866')
191
192
  | 65 | cp866 | Russian MS–DOS |
192
193
  | 66 | cp865 | Nordic MS–DOS |
193
194
  | 67 | cp861 | Icelandic MS–DOS |
195
+ | 68 | cp895 | Kamenický (Czech) MS–DOS |
196
+ | 69 | cp620 | Mazovia (Polish) MS–DOS |
194
197
  | 6a | cp737 | Greek MS–DOS (437G) |
195
198
  | 6b | cp857 | Turkish MS–DOS |
196
199
  | 6c | cp863 | French–Canadian MS–DOS |
@@ -199,9 +202,14 @@ table = DBF::Table.new('dbf/books.dbf', nil, 'cp866')
199
202
  | 7a | cp936 | PRC GBK |
200
203
  | 7b | cp932 | Japanese Shift-JIS |
201
204
  | 7c | cp874 | Thai Windows/MS–DOS |
205
+ | 7d | cp1255 | Hebrew Windows |
206
+ | 7e | cp1256 | Arabic Windows |
202
207
  | 86 | cp737 | Greek OEM |
203
208
  | 87 | cp852 | Slovenian OEM |
204
209
  | 88 | cp857 | Turkish OEM |
210
+ | 96 | macCyrillic | Russian Macintosh |
211
+ | 97 | macCentEuro | Macintosh EE |
212
+ | 98 | macGreek | Greek Macintosh |
205
213
  | c8 | cp1250 | Eastern European Windows |
206
214
  | c9 | cp1251 | Russian Windows |
207
215
  | ca | cp1254 | Turkish Windows |
@@ -328,16 +336,16 @@ at this time.
328
336
  | 04 | dBase IV without memo file | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
329
337
  | 05 | dBase V without memo file | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
330
338
  | 07 | Visual Objects 1.x | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - | - |
331
- | 30 | Visual FoxPro | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | N | Y | N | N | N | N | - |
332
- | 31 | Visual FoxPro with AutoIncrement | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | N | Y | N | N | N | N | N |
333
- | 32 | Visual FoxPro with field type Varchar or Varbinary | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | N | Y | N | N | N | N | N |
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 |
334
342
  | 7b | dBase IV with memo file | Y | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - |
335
343
  | 83 | dBase III with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - |
336
344
  | 87 | Visual Objects 1.x with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | - | - | - | - |
337
345
  | 8b | dBase IV with memo file | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | N | - | - | - |
338
346
  | 8e | dBase IV with SQL table | Y | Y | Y | Y | Y | - | - | - | - | - | - | - | - | N | - | - | - |
339
- | f5 | FoxPro with memo file | Y | Y | Y | Y | Y | Y | Y | Y | N | Y | N | Y | N | N | N | N | N |
340
- | fb | FoxPro without memo file | Y | Y | Y | Y | - | Y | Y | Y | N | Y | N | Y | N | N | N | N | N |
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 |
341
349
 
342
350
  Data type descriptions
343
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
- params = ARGV.getopts('h', 's', 'a', 'c', 'r', 'v')
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/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
@@ -58,8 +58,8 @@ module DBF
58
58
  # @param raw [String]
59
59
  # @yield [raw] for memo column resolution
60
60
  # @return decoded value
61
- def decode(raw, &memo_handler)
62
- type_cast_class.decode(raw, &memo_handler)
61
+ def decode(raw, &)
62
+ type_cast_class.decode(raw, &)
63
63
  end
64
64
 
65
65
  # Returns a Hash with :name, :type, :length, and :decimal keys
@@ -5,8 +5,7 @@ module DBF
5
5
  class Base
6
6
  attr_reader :decimal, :encoding
7
7
 
8
- # @param decimal [Integer]
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
@@ -20,7 +19,7 @@ module DBF
20
19
  false
21
20
  end
22
21
 
23
- def decode(raw, &_memo_handler)
22
+ def decode(raw, &)
24
23
  if skip_blank? && raw.count(' ') == raw.length
25
24
  blank_value
26
25
  else
@@ -71,8 +70,12 @@ module DBF
71
70
  end
72
71
 
73
72
  class Float < Base
73
+ def skip_blank? = true
74
+
74
75
  # @param value [String]
75
76
  def type_cast(value)
77
+ return nil if value.empty?
78
+
76
79
  value.to_f
77
80
  end
78
81
  end
@@ -111,6 +114,8 @@ module DBF
111
114
  # @param value [String]
112
115
  def type_cast(value)
113
116
  days, msecs = value.unpack('l2')
117
+ return nil if days.nil? || days.zero?
118
+
114
119
  secs = (msecs / 1000).to_i
115
120
  ::DateTime.jd(days, (secs / 3600).to_i, (secs / 60).to_i % 60, secs % 60).to_time
116
121
  rescue StandardError
@@ -119,8 +124,8 @@ module DBF
119
124
  end
120
125
 
121
126
  class Memo < Base
122
- def decode(raw, &memo_handler)
123
- memo_content = memo_handler.call(raw)
127
+ def decode(raw, &)
128
+ memo_content = yield(raw)
124
129
  memo_content ? type_cast(memo_content) : nil
125
130
  end
126
131
 
@@ -128,7 +133,7 @@ module DBF
128
133
  def type_cast(value)
129
134
  return value unless encoding && value
130
135
 
131
- value.dup.force_encoding(encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace)
136
+ Encoder.encode(value.dup, encoding)
132
137
  end
133
138
  end
134
139
 
@@ -158,8 +163,9 @@ module DBF
158
163
  private
159
164
 
160
165
  def encode(value)
161
- value.force_encoding(encoding)
162
- @needs_encode ? value.encode(@target_encoding, undef: :replace, invalid: :replace) : value
166
+ return value.force_encoding(encoding) unless @needs_encode
167
+
168
+ Encoder.encode(value, encoding, @target_encoding)
163
169
  end
164
170
  end
165
171
  end
@@ -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
data/lib/dbf/table.rb CHANGED
@@ -140,9 +140,14 @@ module DBF
140
140
  # Dumps all records to a CSV file. If no filename is given then CSV is
141
141
  # output to STDOUT.
142
142
  #
143
- # @param [optional String] path Defaults to STDOUT
144
- def to_csv(path = nil)
145
- csv = CSV.new(path ? File.open(path, 'w') : $stdout, force_quotes: true)
143
+ # @param [optional String, IO] path_or_io String path, IO-like object, or nil for STDOUT
144
+ def to_csv(path_or_io = nil)
145
+ io = case path_or_io
146
+ when nil then $stdout
147
+ when String then File.open(path_or_io, 'w')
148
+ else path_or_io
149
+ end
150
+ csv = CSV.new(io, force_quotes: true)
146
151
  csv << column_names
147
152
  each { |record| csv << record.to_a }
148
153
  end
@@ -159,7 +164,7 @@ module DBF
159
164
  # @param [String] string
160
165
  # @return [String]
161
166
  def encode_string(string) # :nodoc:
162
- string.force_encoding(@encoding).encode(Encoding.default_external, undef: :replace, invalid: :replace)
167
+ Encoder.encode(string, @encoding)
163
168
  end
164
169
 
165
170
  # Encoding specified in the file header
data/lib/dbf/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DBF
4
- VERSION = '5.2.0'
4
+ VERSION = '5.3.0'
5
5
  end
data/lib/dbf.rb CHANGED
@@ -14,6 +14,7 @@ require 'dbf/record_context'
14
14
  require 'dbf/column_type'
15
15
  require 'dbf/column'
16
16
  require 'dbf/encodings'
17
+ require 'dbf/encoder'
17
18
  require 'dbf/header'
18
19
  require 'dbf/version_config'
19
20
  require 'dbf/file_handler'
@@ -25,3 +26,4 @@ require 'dbf/memo/dbase3'
25
26
  require 'dbf/memo/dbase4'
26
27
  require 'dbf/memo/foxpro'
27
28
  require 'dbf/database/foxpro'
29
+ require 'dbf/cli'
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbf
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.2.0
4
+ version: 5.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith Morrison
@@ -37,10 +37,12 @@ files:
37
37
  - bin/dbf
38
38
  - dbf.gemspec
39
39
  - lib/dbf.rb
40
+ - lib/dbf/cli.rb
40
41
  - lib/dbf/column.rb
41
42
  - lib/dbf/column_builder.rb
42
43
  - lib/dbf/column_type.rb
43
44
  - lib/dbf/database/foxpro.rb
45
+ - lib/dbf/encoder.rb
44
46
  - lib/dbf/encodings.rb
45
47
  - lib/dbf/file_handler.rb
46
48
  - lib/dbf/find.rb
@@ -77,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
77
79
  - !ruby/object:Gem::Version
78
80
  version: '0'
79
81
  requirements: []
80
- rubygems_version: 3.7.2
82
+ rubygems_version: 4.0.10
81
83
  specification_version: 4
82
84
  summary: Read xBase files
83
85
  test_files: []