dbf 2.0.7 → 2.0.8

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.
data/Rakefile CHANGED
@@ -1,4 +1,3 @@
1
- require 'rubygems'
2
1
  require 'bundler/setup';
3
2
  Bundler.setup(:default, :development)
4
3
 
@@ -17,9 +16,3 @@ desc "Open an irb session preloaded with this library"
17
16
  task :console do
18
17
  sh "irb -rubygems -I lib -r dbf.rb"
19
18
  end
20
-
21
- # require 'metric_fu'
22
- # MetricFu::Configuration.run do |config|
23
- # config.rcov[:test_files] = ['spec/**/*_spec.rb']
24
- # config.rcov[:rcov_opts] << "-Ispec"
25
- # end
data/dbf.gemspec CHANGED
@@ -14,22 +14,11 @@ Gem::Specification.new do |s|
14
14
 
15
15
  s.executables = ['dbf']
16
16
  s.rdoc_options = ['--charset=UTF-8']
17
- s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'MIT-LICENSE']
17
+ s.extra_rdoc_files = ['README.md', 'CHANGELOG.md', 'LICENSE']
18
18
  s.files = Dir['[A-Z]*', '{bin,docs,lib,spec}/**/*', 'dbf.gemspec']
19
19
  s.test_files = Dir.glob('spec/**/*_spec.rb')
20
20
  s.require_paths = ['lib']
21
21
 
22
22
  s.required_rubygems_version = '>= 1.3.0'
23
23
  s.add_dependency 'fastercsv', '~> 1.5.4'
24
-
25
- s.add_development_dependency 'rspec'
26
- s.add_development_dependency 'rake', '>= 0.9.2'
27
-
28
- # if RUBY_VERSION.to_f >= 1.9
29
- # s.add_development_dependency 'ruby-debug19'
30
- # elsif RUBY_VERSION != '1.8.6'
31
- # s.add_development_dependency 'ruby-debug'
32
- # end
33
- # s.add_development_dependency 'metric_fu'
34
24
  end
35
-
data/lib/dbf.rb CHANGED
@@ -5,6 +5,7 @@ if CSV.const_defined? :Reader
5
5
  require 'fastercsv'
6
6
  end
7
7
 
8
+ require 'dbf/schema'
8
9
  require 'dbf/record'
9
10
  require 'dbf/column/base'
10
11
  require 'dbf/column/dbase'
@@ -7,7 +7,7 @@ module DBF
7
7
  class NameError < StandardError; end
8
8
 
9
9
  class Base
10
- attr_reader :name, :type, :length, :decimal
10
+ attr_reader :table, :name, :type, :length, :decimal
11
11
 
12
12
  # Initialize a new DBF::Column
13
13
  #
@@ -15,11 +15,22 @@ module DBF
15
15
  # @param [String] type
16
16
  # @param [Fixnum] length
17
17
  # @param [Fixnum] decimal
18
- def initialize(name, type, length, decimal, version, encoding=nil)
19
- @name, @type, @length, @decimal, @version, @encoding = clean(name), type, length, decimal, version, encoding
18
+ def initialize(table, name, type, length, decimal)
19
+ @table = table
20
+ @name = clean(name)
21
+ @type = type
22
+ @length = length
23
+ @decimal = decimal
24
+ @version = table.version
25
+ @encoding = table.encoding
26
+
27
+ unless length > 0
28
+ raise LengthError, "field length must be greater than 0"
29
+ end
20
30
 
21
- raise LengthError, "field length must be greater than 0" unless length > 0
22
- raise NameError, "column name cannot be empty" if @name.length == 0
31
+ if @name.empty?
32
+ raise NameError, "column name cannot be empty"
33
+ end
23
34
  end
24
35
 
25
36
  # Cast value to native type
@@ -31,7 +42,7 @@ module DBF
31
42
  when 'N' then unpack_number(value)
32
43
  when 'I' then unpack_unsigned_long(value)
33
44
  when 'F' then value.to_f
34
- when 'Y' then unpack_unsigned_long(value) / 10000.0
45
+ when 'Y' then (unpack_unsigned_long(value) / 10000.0).to_f
35
46
  when 'D' then decode_date(value)
36
47
  when 'T' then decode_datetime(value)
37
48
  when 'L' then boolean(value)
@@ -40,6 +51,9 @@ module DBF
40
51
  end
41
52
  end
42
53
 
54
+ # Returns true if the column is a memo
55
+ #
56
+ # @return [Boolean]
43
57
  def memo?
44
58
  @memo ||= type == 'M'
45
59
  end
@@ -51,16 +65,20 @@ module DBF
51
65
  "\"#{underscored_name}\", #{schema_data_type}\n"
52
66
  end
53
67
 
54
- def self.underscore_name(string)
55
- string.gsub(/::/, '/').
56
- gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
57
- gsub(/([a-z\d])([A-Z])/,'\1_\2').
58
- tr('-', '_').
59
- downcase
60
- end
61
-
68
+ # Underscored name
69
+ #
70
+ # This is the column name converted to underscore format.
71
+ # For example, MyColumn will be returned as my_column.
72
+ #
73
+ # @return [String]
62
74
  def underscored_name
63
- @underscored_name ||= self.class.underscore_name(name)
75
+ @underscored_name ||= begin
76
+ name.gsub(/::/, '/').
77
+ gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
78
+ gsub(/([a-z\d])([A-Z])/,'\1_\2').
79
+ tr('-', '_').
80
+ downcase
81
+ end
64
82
  end
65
83
 
66
84
  private
@@ -73,9 +91,11 @@ module DBF
73
91
  end
74
92
 
75
93
  def decode_datetime(value) #nodoc
76
- days, milliseconds = value.unpack('l2')
77
- seconds = (milliseconds / 1000).to_i
78
- DateTime.jd(days, (seconds/3600).to_i, (seconds/60).to_i % 60, seconds % 60) rescue nil
94
+ days, msecs = value.unpack('l2')
95
+ secs = (msecs / 1000).to_i
96
+ DateTime.jd(days, (secs/3600).to_i, (secs/60).to_i % 60, secs % 60)
97
+ rescue
98
+ nil
79
99
  end
80
100
 
81
101
  def decode_memo(value) #nodoc
@@ -95,10 +115,10 @@ module DBF
95
115
  end
96
116
 
97
117
  def encode_string(value) #nodoc
98
- if @encoding
99
- if String.new.respond_to?(:encoding)
100
- value.force_encoding(@encoding).encode(Encoding.default_external, :undef => :replace, :invalid => :replace)
101
- else
118
+ if @encoding && table.supports_encoding?
119
+ if table.supports_string_encoding?
120
+ value.force_encoding(@encoding).encode(*encoding_args)
121
+ elsif table.supports_iconv?
102
122
  Iconv.conv('UTF-8', @encoding, value)
103
123
  end
104
124
  else
@@ -106,6 +126,10 @@ module DBF
106
126
  end
107
127
  end
108
128
 
129
+ def encoding_args #nodoc
130
+ [Encoding.default_external, {:undef => :replace, :invalid => :replace}]
131
+ end
132
+
109
133
  def schema_data_type #nodoc
110
134
  case type
111
135
  when "N", "F"
@@ -134,9 +158,8 @@ module DBF
134
158
  end
135
159
 
136
160
  def clean(value) #nodoc
137
- first_null = value.index("\x00")
138
- value = value[0, first_null] if first_null
139
- value.gsub(/[^\x20-\x7E]/, "")
161
+ truncated_value = value.strip.partition("\x00").first
162
+ truncated_value.gsub(/[^\x20-\x7E]/, '')
140
163
  end
141
164
 
142
165
  end
@@ -1,9 +1,9 @@
1
1
  module DBF
2
2
  module Column
3
3
  class Foxpro < Base
4
- def unpack_binary(value) #nodoc
5
- value.unpack('d')[0]
6
- end
4
+ # def unpack_binary(value) #nodoc
5
+ # value.unpack('d')[0]
6
+ # end
7
7
  end
8
8
  end
9
- end
9
+ end
data/lib/dbf/header.rb CHANGED
@@ -10,8 +10,12 @@ module DBF
10
10
 
11
11
  def initialize(data, set_encoding)
12
12
  @data = data
13
- @version, @record_count, @header_length, @record_length, @encoding_key = data.unpack("H2 x3 V v2 x17H2")
13
+ @version, @record_count, @header_length, @record_length, @encoding_key = unpack_header
14
14
  @encoding = DBF::ENCODINGS[@encoding_key] if set_encoding
15
15
  end
16
+
17
+ def unpack_header
18
+ data.unpack("H2 x3 V v2 x17H2")
19
+ end
16
20
  end
17
21
  end
data/lib/dbf/memo/base.rb CHANGED
@@ -5,7 +5,7 @@ module DBF
5
5
  BLOCK_SIZE = 512
6
6
 
7
7
  def self.open(filename, version)
8
- self.new File.open(filename, 'rb'), version
8
+ new(File.open(filename, 'rb'), version)
9
9
  end
10
10
 
11
11
  def initialize(data, version)
@@ -45,4 +45,4 @@ module DBF
45
45
  end
46
46
  end
47
47
  end
48
- end
48
+ end
data/lib/dbf/record.rb CHANGED
@@ -36,40 +36,49 @@ module DBF
36
36
  end
37
37
 
38
38
  # Reads attributes by column name
39
+ #
40
+ # @param [String, Symbol] key
39
41
  def [](key)
40
42
  key = key.to_s
41
43
  if attributes.has_key?(key)
42
44
  attributes[key]
43
- elsif index = column_names.index(key)
45
+ elsif index = underscored_column_names.index(key)
44
46
  attributes[@columns[index].name]
45
47
  end
46
48
  end
47
49
 
50
+ # Record attributes
51
+ #
48
52
  # @return [Hash]
49
53
  def attributes
50
54
  @attributes ||= Hash[@columns.map {|column| [column.name, init_attribute(column)]}]
51
55
  end
52
56
 
57
+ # Overrides standard Object.respond_to? to return true if a
58
+ # matching column name is found.
59
+ #
60
+ # @param [String, Symbol] method
61
+ # @return [Boolean]
53
62
  def respond_to?(method, *args)
54
- if column_names.include?(method.to_s)
63
+ if underscored_column_names.include?(method.to_s)
55
64
  true
56
65
  else
57
66
  super
58
67
  end
59
68
  end
60
69
 
61
- def method_missing(method, *args)
62
- if index = column_names.index(method.to_s)
70
+ private
71
+
72
+ def method_missing(method, *args) #nodoc
73
+ if index = underscored_column_names.index(method.to_s)
63
74
  attributes[@columns[index].name]
64
75
  else
65
76
  super
66
77
  end
67
78
  end
68
79
 
69
- private
70
-
71
- def column_names
72
- @column_names ||= @columns.map {|column| column.underscored_name}
80
+ def underscored_column_names # nodoc
81
+ @underscored_column_names ||= @columns.map {|column| column.underscored_name}
73
82
  end
74
83
 
75
84
  def init_attribute(column) #nodoc
data/lib/dbf/schema.rb ADDED
@@ -0,0 +1,34 @@
1
+ module DBF
2
+ module Schema
3
+ # Generate an ActiveRecord::Schema
4
+ #
5
+ # xBase data types are converted to generic types as follows:
6
+ # - Number columns with no decimals are converted to :integer
7
+ # - Number columns with decimals are converted to :float
8
+ # - Date columns are converted to :datetime
9
+ # - Logical columns are converted to :boolean
10
+ # - Memo columns are converted to :text
11
+ # - Character columns are converted to :string and the :limit option is set
12
+ # to the length of the character column
13
+ #
14
+ # Example:
15
+ # create_table "mydata" do |t|
16
+ # t.column :name, :string, :limit => 30
17
+ # t.column :last_update, :datetime
18
+ # t.column :is_active, :boolean
19
+ # t.column :age, :integer
20
+ # t.column :notes, :text
21
+ # end
22
+ #
23
+ # @return [String]
24
+ def schema
25
+ s = "ActiveRecord::Schema.define do\n"
26
+ s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
27
+ columns.each do |column|
28
+ s << " t.column #{column.schema_definition}"
29
+ end
30
+ s << " end\nend"
31
+ s
32
+ end
33
+ end
34
+ end
data/lib/dbf/table.rb CHANGED
@@ -1,9 +1,12 @@
1
1
  module DBF
2
+ class FileNotFoundError < StandardError
3
+ end
2
4
 
3
5
  # DBF::Table is the primary interface to a single DBF file and provides
4
6
  # methods for enumerating and searching the records.
5
7
  class Table
6
8
  include Enumerable
9
+ include Schema
7
10
 
8
11
  DBF_HEADER_SIZE = 32
9
12
 
@@ -59,11 +62,15 @@ module DBF
59
62
  # @param [optional String, StringIO] memo Path to the memo file or a StringIO object
60
63
  # @param [optional String, Encoding] encoding Name of the encoding or an Encoding object
61
64
  def initialize(data, memo = nil, encoding = nil)
62
- @data = open_data(data)
63
- @data.rewind
64
- @header = Header.new(@data.read(DBF_HEADER_SIZE), supports_encoding? || supports_iconv?)
65
- @encoding = encoding || header.encoding
66
- @memo = open_memo(data, memo)
65
+ begin
66
+ @data = open_data(data)
67
+ @data.rewind
68
+ @header = Header.new(@data.read(DBF_HEADER_SIZE), supports_encoding?)
69
+ @encoding = encoding || header.encoding
70
+ @memo = open_memo(data, memo)
71
+ rescue Errno::ENOENT => error
72
+ raise DBF::FileNotFoundError.new("file not found: #{data}")
73
+ end
67
74
  end
68
75
 
69
76
  # @return [TrueClass, FalseClass]
@@ -108,7 +115,7 @@ module DBF
108
115
  # @param [Fixnum] index
109
116
  # @return [DBF::Record, NilClass]
110
117
  def record(index)
111
- seek(index * header.record_length)
118
+ seek_to_record(index)
112
119
  if !deleted_record?
113
120
  DBF::Record.new(@data.read(header.record_length), columns, version, @memo)
114
121
  end
@@ -137,37 +144,6 @@ module DBF
137
144
  VERSIONS[version]
138
145
  end
139
146
 
140
- # Generate an ActiveRecord::Schema
141
- #
142
- # xBase data types are converted to generic types as follows:
143
- # - Number columns with no decimals are converted to :integer
144
- # - Number columns with decimals are converted to :float
145
- # - Date columns are converted to :datetime
146
- # - Logical columns are converted to :boolean
147
- # - Memo columns are converted to :text
148
- # - Character columns are converted to :string and the :limit option is set
149
- # to the length of the character column
150
- #
151
- # Example:
152
- # create_table "mydata" do |t|
153
- # t.column :name, :string, :limit => 30
154
- # t.column :last_update, :datetime
155
- # t.column :is_active, :boolean
156
- # t.column :age, :integer
157
- # t.column :notes, :text
158
- # end
159
- #
160
- # @return [String]
161
- def schema
162
- s = "ActiveRecord::Schema.define do\n"
163
- s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
164
- columns.each do |column|
165
- s << " t.column #{column.schema_definition}"
166
- end
167
- s << " end\nend"
168
- s
169
- end
170
-
171
147
  # Dumps all records to a CSV file. If no filename is given then CSV is
172
148
  # output to STDOUT.
173
149
  #
@@ -215,38 +191,58 @@ module DBF
215
191
  end
216
192
  end
217
193
 
218
- # Retrieves column information from the database
194
+ # All columns
195
+ #
196
+ # @return [Array]
219
197
  def columns
220
- @columns ||= begin
221
- @data.seek(DBF_HEADER_SIZE)
222
- columns = []
223
- while !["\0", "\r"].include?(first_byte = @data.read(1))
224
- column_data = first_byte + @data.read(31)
225
- name, type, length, decimal = column_data.unpack('a10 x a x4 C2')
226
- if length > 0
227
- columns << column_class.new(name.strip, type, length, decimal, version, encoding)
228
- end
229
- end
230
- columns
231
- end
198
+ @columns ||= build_columns
232
199
  end
233
200
 
201
+ # Column names
202
+ #
203
+ # @return [String]
204
+ def column_names
205
+ columns.map { |column| column.name }
206
+ end
207
+
208
+ # Is string encoding supported?
209
+ # String encoding is always supported in Ruby 1.9+.
210
+ # Ruby 1.8.x requires that Ruby be compiled with iconv support.
234
211
  def supports_encoding?
235
- String.new.respond_to?(:encoding)
212
+ supports_string_encoding? || supports_iconv?
213
+ end
214
+
215
+ # Does String support encoding? Should be true in Ruby 1.9+
216
+ def supports_string_encoding?
217
+ ''.respond_to?(:encoding)
236
218
  end
237
219
 
238
- def supports_iconv?
220
+ def supports_iconv? #nodoc
239
221
  require 'iconv'
240
222
  true
241
223
  rescue
242
224
  false
243
225
  end
244
226
 
245
- def foxpro?
246
- FOXPRO_VERSIONS.keys.include? version
227
+ private
228
+
229
+ def build_columns #nodoc
230
+ columns = []
231
+ @data.seek(DBF_HEADER_SIZE)
232
+ while !["\0", "\r"].include?(first_byte = @data.read(1))
233
+ column_data = first_byte + @data.read(DBF_HEADER_SIZE - 1)
234
+ name, type, length, decimal = column_data.unpack('a10 x a x4 C2')
235
+ if length > 0
236
+ columns << column_class.new(self, name, type, length, decimal)
237
+ end
238
+ end
239
+ columns
247
240
  end
248
241
 
249
- private
242
+
243
+ def foxpro? #nodoc
244
+ FOXPRO_VERSIONS.keys.include? version
245
+ end
250
246
 
251
247
  def column_class #nodoc
252
248
  @column_class ||= foxpro? ? Column::Foxpro : Column::Dbase
@@ -312,6 +308,10 @@ module DBF
312
308
  @data.seek header.header_length + offset
313
309
  end
314
310
 
311
+ def seek_to_record(index) #nodoc
312
+ seek(index * header.record_length)
313
+ end
314
+
315
315
  def csv_class #nodoc
316
316
  @csv_class ||= CSV.const_defined?(:Reader) ? FCSV : CSV
317
317
  end