dbf 2.0.7 → 2.0.8

Sign up to get free protection for your applications and to get access to all the features.
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