dbf 3.1.0 → 3.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +3 -0
- data/Gemfile.lock +14 -5
- data/LICENSE +1 -1
- data/README.md +7 -11
- data/lib/dbf/column.rb +32 -45
- data/lib/dbf/column_type.rb +1 -4
- data/lib/dbf/database/foxpro.rb +2 -3
- data/lib/dbf/record.rb +33 -39
- data/lib/dbf/schema.rb +48 -5
- data/lib/dbf/table.rb +94 -93
- data/lib/dbf/version.rb +1 -1
- data/spec/dbf/column_spec.rb +4 -52
- data/spec/dbf/file_formats_spec.rb +4 -4
- data/spec/dbf/table_spec.rb +48 -0
- data/spec/spec_helper.rb +2 -2
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a8b06626d3eb46fa746bd21d8b359c099dec90b2
|
4
|
+
data.tar.gz: e1da4bd35cc876d257552581d1d41079d55fc15c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1496225ad7fbbc044287627d2a13196e70614d4adbd7467911b51bd4860ce6e35f86c86713c5ec05dbed19a0c956bad80e2906ea8c6541ffd4f6a26764e73304
|
7
|
+
data.tar.gz: 23f28160bb106378de8e7ffe409ab1db78d31d6c8f380a5a2e21e05f820b48b86030dba346e6d5c461cb292cfbd640c7562ec5a653e29055ea3822166bf97593
|
data/CHANGELOG.md
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
dbf (3.
|
4
|
+
dbf (3.1.1)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
@@ -10,6 +10,7 @@ GEM
|
|
10
10
|
coderay (1.1.0)
|
11
11
|
diff-lcs (1.2.5)
|
12
12
|
ffi (1.9.10)
|
13
|
+
ffi (1.9.10-java)
|
13
14
|
formatador (0.2.5)
|
14
15
|
guard (2.13.0)
|
15
16
|
formatador (>= 0.2.4)
|
@@ -25,7 +26,7 @@ GEM
|
|
25
26
|
guard (~> 2.1)
|
26
27
|
guard-compat (~> 1.1)
|
27
28
|
rspec (>= 2.99.0, < 4.0)
|
28
|
-
listen (3.0.
|
29
|
+
listen (3.0.5)
|
29
30
|
rb-fsevent (>= 0.9.3)
|
30
31
|
rb-inotify (>= 0.9)
|
31
32
|
lumberjack (1.0.9)
|
@@ -34,11 +35,16 @@ GEM
|
|
34
35
|
notiffany (0.0.8)
|
35
36
|
nenv (~> 0.1)
|
36
37
|
shellany (~> 0.0)
|
37
|
-
pry (0.10.
|
38
|
+
pry (0.10.3)
|
38
39
|
coderay (~> 1.1.0)
|
39
40
|
method_source (~> 0.8.1)
|
40
41
|
slop (~> 3.4)
|
41
|
-
|
42
|
+
pry (0.10.3-java)
|
43
|
+
coderay (~> 1.1.0)
|
44
|
+
method_source (~> 0.8.1)
|
45
|
+
slop (~> 3.4)
|
46
|
+
spoon (~> 0.0)
|
47
|
+
rake (11.2.2)
|
42
48
|
rb-fsevent (0.9.6)
|
43
49
|
rb-inotify (0.9.5)
|
44
50
|
ffi (>= 0.5.0)
|
@@ -57,9 +63,12 @@ GEM
|
|
57
63
|
rspec-support (3.4.0)
|
58
64
|
shellany (0.0.1)
|
59
65
|
slop (3.6.0)
|
66
|
+
spoon (0.0.4)
|
67
|
+
ffi
|
60
68
|
thor (0.19.1)
|
61
69
|
|
62
70
|
PLATFORMS
|
71
|
+
java
|
63
72
|
ruby
|
64
73
|
|
65
74
|
DEPENDENCIES
|
@@ -71,4 +80,4 @@ DEPENDENCIES
|
|
71
80
|
rspec
|
72
81
|
|
73
82
|
BUNDLED WITH
|
74
|
-
1.
|
83
|
+
1.13.7
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -22,8 +22,8 @@ NOTE: beginning with version 3 we have dropped support for Ruby 1.8 and 1.9. If
|
|
22
22
|
|
23
23
|
DBF is tested to work with the following versions of Ruby:
|
24
24
|
|
25
|
-
* MRI Ruby 2.0.x, 2.1.x, 2.2.x, 2.3.x
|
26
|
-
* JRuby
|
25
|
+
* MRI Ruby 2.0.x, 2.1.x, 2.2.x, 2.3.x, 2.4.x
|
26
|
+
* JRuby 1.7.x
|
27
27
|
|
28
28
|
## Installation
|
29
29
|
|
@@ -79,21 +79,17 @@ widget = widgets.find(6)
|
|
79
79
|
Note that find() will return nil if the requested record has been deleted
|
80
80
|
and not yet pruned from the database.
|
81
81
|
|
82
|
-
The value for
|
83
|
-
ways
|
82
|
+
The value for an attribute can be accessed via element reference in several
|
83
|
+
ways.
|
84
84
|
|
85
85
|
```ruby
|
86
|
+
widget.slot_number # underscored field name as method
|
87
|
+
|
86
88
|
widget["SlotNumber"] # original field name in dbf file
|
87
89
|
widget['slot_number'] # underscored field name string
|
88
90
|
widget[:slot_number] # underscored field name symbol
|
89
91
|
```
|
90
92
|
|
91
|
-
Attributes can also be accessed as method using the underscored field name
|
92
|
-
|
93
|
-
```ruby
|
94
|
-
widget.slot_number
|
95
|
-
```
|
96
|
-
|
97
93
|
Get a hash of all attributes. The keys are the original column names.
|
98
94
|
|
99
95
|
```ruby
|
@@ -276,7 +272,7 @@ for a full list of supported column types.
|
|
276
272
|
|
277
273
|
## License
|
278
274
|
|
279
|
-
Copyright (c) 2006-
|
275
|
+
Copyright (c) 2006-2017 Keith Morrison <<keithm@infused.org>>
|
280
276
|
|
281
277
|
Permission is hereby granted, free of charge, to any person
|
282
278
|
obtaining a copy of this software and associated documentation
|
data/lib/dbf/column.rb
CHANGED
@@ -1,10 +1,10 @@
|
|
1
1
|
module DBF
|
2
2
|
class Column
|
3
|
-
|
4
|
-
|
3
|
+
class LengthError < StandardError
|
4
|
+
end
|
5
5
|
|
6
|
-
|
7
|
-
|
6
|
+
class NameError < StandardError
|
7
|
+
end
|
8
8
|
|
9
9
|
attr_reader :table, :name, :type, :length, :decimal
|
10
10
|
|
@@ -26,8 +26,8 @@ module DBF
|
|
26
26
|
#
|
27
27
|
# @param [String] name
|
28
28
|
# @param [String] type
|
29
|
-
# @param [
|
30
|
-
# @param [
|
29
|
+
# @param [Integer] length
|
30
|
+
# @param [Integer] decimal
|
31
31
|
def initialize(table, name, type, length, decimal)
|
32
32
|
@table = table
|
33
33
|
@name = clean(name)
|
@@ -41,14 +41,6 @@ module DBF
|
|
41
41
|
validate_name
|
42
42
|
end
|
43
43
|
|
44
|
-
# Cast value to native type
|
45
|
-
#
|
46
|
-
# @param [String] value
|
47
|
-
# @return [Fixnum, Float, Date, DateTime, Boolean, String]
|
48
|
-
def type_cast(value)
|
49
|
-
type_cast_class.type_cast(value)
|
50
|
-
end
|
51
|
-
|
52
44
|
# Returns true if the column is a memo
|
53
45
|
#
|
54
46
|
# @return [Boolean]
|
@@ -56,18 +48,19 @@ module DBF
|
|
56
48
|
@memo ||= type == 'M'
|
57
49
|
end
|
58
50
|
|
59
|
-
#
|
51
|
+
# Returns a Hash with :name, :type, :length, and :decimal keys
|
60
52
|
#
|
61
|
-
# @return [
|
62
|
-
def
|
63
|
-
|
53
|
+
# @return [Hash]
|
54
|
+
def to_hash
|
55
|
+
{name: name, type: type, length: length, decimal: decimal}
|
64
56
|
end
|
65
57
|
|
66
|
-
#
|
58
|
+
# Cast value to native type
|
67
59
|
#
|
68
|
-
# @
|
69
|
-
|
70
|
-
|
60
|
+
# @param [String] value
|
61
|
+
# @return [Integer, Float, Date, DateTime, Boolean, String]
|
62
|
+
def type_cast(value)
|
63
|
+
type_cast_class.type_cast(value)
|
71
64
|
end
|
72
65
|
|
73
66
|
# Underscored name
|
@@ -78,27 +71,19 @@ module DBF
|
|
78
71
|
# @return [String]
|
79
72
|
def underscored_name
|
80
73
|
@underscored_name ||= begin
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
un.downcase!
|
87
|
-
un
|
74
|
+
name.gsub(/::/, '/')
|
75
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
76
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
77
|
+
.tr('-', '_')
|
78
|
+
.downcase
|
88
79
|
end
|
89
80
|
end
|
90
81
|
|
91
|
-
def to_hash
|
92
|
-
{name: name, type: type, length: length, decimal: decimal}
|
93
|
-
end
|
94
|
-
|
95
82
|
private
|
96
83
|
|
97
|
-
def
|
98
|
-
|
99
|
-
|
100
|
-
klass.new(@decimal, @encoding)
|
101
|
-
end
|
84
|
+
def clean(value) # nodoc
|
85
|
+
truncated_value = value.strip.partition("\x00").first
|
86
|
+
truncated_value.gsub(/[^\x20-\x7E]/, '')
|
102
87
|
end
|
103
88
|
|
104
89
|
def encode(value, strip_output = false) # nodoc
|
@@ -108,10 +93,6 @@ module DBF
|
|
108
93
|
strip_output ? output.strip : output
|
109
94
|
end
|
110
95
|
|
111
|
-
def encode_string(string) # nodoc
|
112
|
-
string.force_encoding(@encoding).encode(*encoding_args)
|
113
|
-
end
|
114
|
-
|
115
96
|
def encoding_args # nodoc
|
116
97
|
@encoding_args ||= [
|
117
98
|
Encoding.default_external,
|
@@ -119,6 +100,10 @@ module DBF
|
|
119
100
|
]
|
120
101
|
end
|
121
102
|
|
103
|
+
def encode_string(string) # nodoc
|
104
|
+
string.force_encoding(@encoding).encode(*encoding_args)
|
105
|
+
end
|
106
|
+
|
122
107
|
def schema_data_type(format = :activerecord) # nodoc
|
123
108
|
case type
|
124
109
|
when 'N', 'F'
|
@@ -146,9 +131,11 @@ module DBF
|
|
146
131
|
end
|
147
132
|
end
|
148
133
|
|
149
|
-
def
|
150
|
-
|
151
|
-
|
134
|
+
def type_cast_class # nodoc
|
135
|
+
@type_cast_class ||= begin
|
136
|
+
klass = @length == 0 ? ColumnType::Nil : TYPE_CAST_CLASS[type.to_sym]
|
137
|
+
klass.new(@decimal, @encoding)
|
138
|
+
end
|
152
139
|
end
|
153
140
|
|
154
141
|
def validate_length # nodoc
|
data/lib/dbf/column_type.rb
CHANGED
data/lib/dbf/database/foxpro.rb
CHANGED
@@ -43,9 +43,8 @@ module DBF
|
|
43
43
|
# on any platform.
|
44
44
|
# @return String
|
45
45
|
def table_path(name)
|
46
|
-
|
47
|
-
|
48
|
-
path = Dir.glob(glob).find { |match| match.downcase == example.downcase }
|
46
|
+
glob = File.join(@dirname, "#{name}.dbf")
|
47
|
+
path = Dir.glob(glob, File::FNM_CASEFOLD).first
|
49
48
|
|
50
49
|
unless path && File.exist?(path)
|
51
50
|
raise DBF::FileNotFoundError, "related table not found: #{name}"
|
data/lib/dbf/record.rb
CHANGED
@@ -22,21 +22,6 @@ module DBF
|
|
22
22
|
other.respond_to?(:attributes) && other.attributes == attributes
|
23
23
|
end
|
24
24
|
|
25
|
-
# Maps a row to an array of values
|
26
|
-
#
|
27
|
-
# @return [Array]
|
28
|
-
def to_a
|
29
|
-
@columns.map { |column| attributes[column.name] }
|
30
|
-
end
|
31
|
-
|
32
|
-
# Do all search parameters match?
|
33
|
-
#
|
34
|
-
# @param [Hash] options
|
35
|
-
# @return [Boolean]
|
36
|
-
def match?(options)
|
37
|
-
options.all? { |key, value| self[key] == value }
|
38
|
-
end
|
39
|
-
|
40
25
|
# Reads attributes by column name
|
41
26
|
#
|
42
27
|
# @param [String, Symbol] key
|
@@ -56,6 +41,14 @@ module DBF
|
|
56
41
|
@attributes ||= Hash[attribute_map]
|
57
42
|
end
|
58
43
|
|
44
|
+
# Do all search parameters match?
|
45
|
+
#
|
46
|
+
# @param [Hash] options
|
47
|
+
# @return [Boolean]
|
48
|
+
def match?(options)
|
49
|
+
options.all? { |key, value| self[key] == value }
|
50
|
+
end
|
51
|
+
|
59
52
|
# Overrides standard Object.respond_to? to return true if a
|
60
53
|
# matching column name is found.
|
61
54
|
#
|
@@ -65,36 +58,24 @@ module DBF
|
|
65
58
|
underscored_column_names.include?(method.to_s) || super
|
66
59
|
end
|
67
60
|
|
61
|
+
# Maps a row to an array of values
|
62
|
+
#
|
63
|
+
# @return [Array]
|
64
|
+
def to_a
|
65
|
+
@columns.map { |column| attributes[column.name] }
|
66
|
+
end
|
67
|
+
|
68
68
|
private
|
69
69
|
|
70
70
|
def attribute_map # nodoc
|
71
71
|
@columns.map { |column| [column.name, init_attribute(column)] }
|
72
72
|
end
|
73
73
|
|
74
|
-
def
|
75
|
-
|
76
|
-
index = @columns.index(column)
|
77
|
-
@columns[0, index + 1].compact.reduce(0) { |x, c| x += c.length }
|
78
|
-
end
|
79
|
-
|
80
|
-
def method_missing(method, *args) # nodoc
|
81
|
-
if (index = underscored_column_names.index(method.to_s))
|
82
|
-
attributes[@columns[index].name]
|
83
|
-
else
|
84
|
-
super
|
85
|
-
end
|
86
|
-
end
|
87
|
-
|
88
|
-
def underscored_column_names # nodoc
|
89
|
-
@underscored_column_names ||= @columns.map(&:underscored_name)
|
90
|
-
end
|
91
|
-
|
92
|
-
def init_attribute(column) # nodoc
|
93
|
-
value = column.memo? ? memo(column) : get_data(column)
|
94
|
-
column.type_cast(value)
|
74
|
+
def get_data(column) # nodoc
|
75
|
+
@data.read(column.length)
|
95
76
|
end
|
96
77
|
|
97
|
-
def
|
78
|
+
def get_memo(column) # nodoc
|
98
79
|
if @memo
|
99
80
|
@memo.get(memo_start_block(column))
|
100
81
|
else
|
@@ -104,6 +85,11 @@ module DBF
|
|
104
85
|
end
|
105
86
|
end
|
106
87
|
|
88
|
+
def init_attribute(column) # nodoc
|
89
|
+
value = column.memo? ? get_memo(column) : get_data(column)
|
90
|
+
column.type_cast(value)
|
91
|
+
end
|
92
|
+
|
107
93
|
def memo_start_block(column) # nodoc
|
108
94
|
data = get_data(column)
|
109
95
|
if %w(30 31).include?(@version)
|
@@ -112,8 +98,16 @@ module DBF
|
|
112
98
|
data.to_i
|
113
99
|
end
|
114
100
|
|
115
|
-
def
|
116
|
-
|
101
|
+
def method_missing(method, *args) # nodoc
|
102
|
+
if (index = underscored_column_names.index(method.to_s))
|
103
|
+
attributes[@columns[index].name]
|
104
|
+
else
|
105
|
+
super
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def underscored_column_names # nodoc
|
110
|
+
@underscored_column_names ||= @columns.map(&:underscored_name)
|
117
111
|
end
|
118
112
|
end
|
119
113
|
end
|
data/lib/dbf/schema.rb
CHANGED
@@ -32,23 +32,23 @@ module DBF
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
-
def activerecord_schema(_table_only = false)
|
35
|
+
def activerecord_schema(_table_only = false) # nodoc
|
36
36
|
s = "ActiveRecord::Schema.define do\n"
|
37
37
|
s << " create_table \"#{name}\" do |t|\n"
|
38
38
|
columns.each do |column|
|
39
|
-
s << " t.column #{column
|
39
|
+
s << " t.column #{activerecord_schema_definition(column)}"
|
40
40
|
end
|
41
41
|
s << " end\nend"
|
42
42
|
s
|
43
43
|
end
|
44
44
|
|
45
|
-
def sequel_schema(table_only = false)
|
45
|
+
def sequel_schema(table_only = false) # nodoc
|
46
46
|
s = ''
|
47
47
|
s << "Sequel.migration do\n" unless table_only
|
48
48
|
s << " change do\n " unless table_only
|
49
49
|
s << " create_table(:#{name}) do\n"
|
50
50
|
columns.each do |column|
|
51
|
-
s << " column #{column
|
51
|
+
s << " column #{sequel_schema_definition(column)}"
|
52
52
|
end
|
53
53
|
s << " end\n"
|
54
54
|
s << " end\n" unless table_only
|
@@ -56,8 +56,51 @@ module DBF
|
|
56
56
|
s
|
57
57
|
end
|
58
58
|
|
59
|
-
def json_schema(_table_only = false)
|
59
|
+
def json_schema(_table_only = false) # nodoc
|
60
60
|
columns.map(&:to_hash).to_json
|
61
61
|
end
|
62
|
+
|
63
|
+
# ActiveRecord schema definition
|
64
|
+
#
|
65
|
+
# @param [DBF::Column]
|
66
|
+
# @return [String]
|
67
|
+
def activerecord_schema_definition(column)
|
68
|
+
"\"#{column.underscored_name}\", #{schema_data_type(column, :activerecord)}\n"
|
69
|
+
end
|
70
|
+
|
71
|
+
# Sequel schema definition
|
72
|
+
#
|
73
|
+
# @params [DBF::Column]
|
74
|
+
# @return [String]
|
75
|
+
def sequel_schema_definition(column)
|
76
|
+
":#{column.underscored_name}, #{schema_data_type(column, :sequel)}\n"
|
77
|
+
end
|
78
|
+
|
79
|
+
def schema_data_type(column, format = :activerecord) # nodoc
|
80
|
+
case column.type
|
81
|
+
when 'N', 'F'
|
82
|
+
column.decimal > 0 ? ':float' : ':integer'
|
83
|
+
when 'I'
|
84
|
+
':integer'
|
85
|
+
when 'Y'
|
86
|
+
':decimal, :precision => 15, :scale => 4'
|
87
|
+
when 'D'
|
88
|
+
':date'
|
89
|
+
when 'T'
|
90
|
+
':datetime'
|
91
|
+
when 'L'
|
92
|
+
':boolean'
|
93
|
+
when 'M'
|
94
|
+
':text'
|
95
|
+
when 'B'
|
96
|
+
':binary'
|
97
|
+
else
|
98
|
+
if format == :sequel
|
99
|
+
":varchar, :size => #{column.length}"
|
100
|
+
else
|
101
|
+
":string, :limit => #{column.length}"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
62
105
|
end
|
63
106
|
end
|
data/lib/dbf/table.rb
CHANGED
@@ -37,7 +37,6 @@ module DBF
|
|
37
37
|
'fb' => 'FoxPro without memo file'
|
38
38
|
}
|
39
39
|
|
40
|
-
attr_reader :header
|
41
40
|
attr_accessor :encoding
|
42
41
|
attr_writer :name
|
43
42
|
|
@@ -64,18 +63,9 @@ module DBF
|
|
64
63
|
# @param [optional String, Encoding] encoding Name of the encoding or an Encoding object
|
65
64
|
def initialize(data, memo = nil, encoding = nil)
|
66
65
|
@data = open_data(data)
|
67
|
-
@data.rewind
|
68
|
-
@header = Header.new(@data.read DBF_HEADER_SIZE)
|
69
66
|
@encoding = encoding || header.encoding
|
70
67
|
@memo = open_memo(data, memo)
|
71
68
|
yield self if block_given?
|
72
|
-
rescue Errno::ENOENT
|
73
|
-
raise DBF::FileNotFoundError, "file not found: #{data}"
|
74
|
-
end
|
75
|
-
|
76
|
-
# @return [TrueClass, FalseClass]
|
77
|
-
def has_memo_file?
|
78
|
-
!!@memo
|
79
69
|
end
|
80
70
|
|
81
71
|
# Closes the table and memo file
|
@@ -95,14 +85,18 @@ module DBF
|
|
95
85
|
end
|
96
86
|
end
|
97
87
|
|
98
|
-
#
|
99
|
-
|
100
|
-
|
88
|
+
# Column names
|
89
|
+
#
|
90
|
+
# @return [String]
|
91
|
+
def column_names
|
92
|
+
columns.map(&:name)
|
101
93
|
end
|
102
94
|
|
103
|
-
#
|
104
|
-
|
105
|
-
|
95
|
+
# All columns
|
96
|
+
#
|
97
|
+
# @return [Array]
|
98
|
+
def columns
|
99
|
+
@columns ||= build_columns
|
106
100
|
end
|
107
101
|
|
108
102
|
# Calls block once for each record in the table. The record may be nil
|
@@ -113,50 +107,9 @@ module DBF
|
|
113
107
|
header.record_count.times { |i| yield record(i) }
|
114
108
|
end
|
115
109
|
|
116
|
-
# Retrieve a record by index number.
|
117
|
-
# The record will be nil if it has been deleted, but not yet pruned from
|
118
|
-
# the database.
|
119
|
-
#
|
120
|
-
# @param [Fixnum] index
|
121
|
-
# @return [DBF::Record, NilClass]
|
122
|
-
def record(index)
|
123
|
-
seek_to_record(index)
|
124
|
-
return nil if deleted_record?
|
125
|
-
DBF::Record.new(@data.read(header.record_length), columns, version, @memo)
|
126
|
-
end
|
127
|
-
|
128
|
-
alias_method :row, :record
|
129
|
-
|
130
|
-
# Internal dBase version number
|
131
|
-
#
|
132
|
-
# @return [String]
|
133
|
-
def version
|
134
|
-
@version ||= header.version
|
135
|
-
end
|
136
|
-
|
137
|
-
# Total number of records
|
138
|
-
#
|
139
|
-
# @return [Fixnum]
|
140
|
-
def record_count
|
141
|
-
@record_count ||= header.record_count
|
142
|
-
end
|
143
|
-
|
144
|
-
# Human readable version description
|
145
|
-
#
|
146
110
|
# @return [String]
|
147
|
-
def
|
148
|
-
|
149
|
-
end
|
150
|
-
|
151
|
-
# Dumps all records to a CSV file. If no filename is given then CSV is
|
152
|
-
# output to STDOUT.
|
153
|
-
#
|
154
|
-
# @param [optional String] path Defaults to STDOUT
|
155
|
-
def to_csv(path = nil)
|
156
|
-
out_io = path ? File.open(path, 'w') : $stdout
|
157
|
-
csv = CSV.new(out_io, force_quotes: true)
|
158
|
-
csv << column_names
|
159
|
-
each { |record| csv << record.to_a }
|
111
|
+
def filename
|
112
|
+
File.basename(@data.path) if @data.respond_to?(:path)
|
160
113
|
end
|
161
114
|
|
162
115
|
# Find records using a simple ActiveRecord-like syntax.
|
@@ -181,12 +134,12 @@ module DBF
|
|
181
134
|
# returned. The equivalent SQL would be "WHERE key1 = 'value1'
|
182
135
|
# AND key2 = 'value2'".
|
183
136
|
#
|
184
|
-
# @param [
|
137
|
+
# @param [Integer, Symbol] command
|
185
138
|
# @param [optional, Hash] options Hash of search parameters
|
186
139
|
# @yield [optional, DBF::Record, NilClass]
|
187
140
|
def find(command, options = {}, &block)
|
188
141
|
case command
|
189
|
-
when
|
142
|
+
when Integer
|
190
143
|
record(command)
|
191
144
|
when Array
|
192
145
|
command.map { |i| record(i) }
|
@@ -197,18 +150,60 @@ module DBF
|
|
197
150
|
end
|
198
151
|
end
|
199
152
|
|
200
|
-
#
|
153
|
+
# @return [TrueClass, FalseClass]
|
154
|
+
def has_memo_file?
|
155
|
+
!!@memo
|
156
|
+
end
|
157
|
+
|
158
|
+
# @return [String]
|
159
|
+
def name
|
160
|
+
@name ||= filename && File.basename(filename, ".*")
|
161
|
+
end
|
162
|
+
|
163
|
+
# Retrieve a record by index number.
|
164
|
+
# The record will be nil if it has been deleted, but not yet pruned from
|
165
|
+
# the database.
|
201
166
|
#
|
202
|
-
# @
|
203
|
-
|
204
|
-
|
167
|
+
# @param [Integer] index
|
168
|
+
# @return [DBF::Record, NilClass]
|
169
|
+
def record(index)
|
170
|
+
seek_to_record(index)
|
171
|
+
return nil if deleted_record?
|
172
|
+
DBF::Record.new(@data.read(header.record_length), columns, version, @memo)
|
205
173
|
end
|
206
174
|
|
207
|
-
|
175
|
+
alias_method :row, :record
|
176
|
+
|
177
|
+
# Total number of records
|
178
|
+
#
|
179
|
+
# @return [Integer]
|
180
|
+
def record_count
|
181
|
+
@record_count ||= header.record_count
|
182
|
+
end
|
183
|
+
|
184
|
+
# Dumps all records to a CSV file. If no filename is given then CSV is
|
185
|
+
# output to STDOUT.
|
186
|
+
#
|
187
|
+
# @param [optional String] path Defaults to STDOUT
|
188
|
+
def to_csv(path = nil)
|
189
|
+
out_io = path ? File.open(path, 'w') : $stdout
|
190
|
+
csv = CSV.new(out_io, force_quotes: true)
|
191
|
+
csv << column_names
|
192
|
+
each { |record| csv << record.to_a }
|
193
|
+
end
|
194
|
+
|
195
|
+
# Internal dBase version number
|
208
196
|
#
|
209
197
|
# @return [String]
|
210
|
-
def
|
211
|
-
|
198
|
+
def version
|
199
|
+
@version ||= header.version
|
200
|
+
end
|
201
|
+
|
202
|
+
# Human readable version description
|
203
|
+
#
|
204
|
+
# @return [String]
|
205
|
+
def version_description
|
206
|
+
VERSIONS[version]
|
212
207
|
end
|
213
208
|
|
214
209
|
private
|
@@ -224,6 +219,11 @@ module DBF
|
|
224
219
|
columns
|
225
220
|
end
|
226
221
|
|
222
|
+
def deleted_record? # nodoc
|
223
|
+
flag = @data.read(1)
|
224
|
+
flag ? flag.unpack('a') == ['*'] : true
|
225
|
+
end
|
226
|
+
|
227
227
|
def end_of_record? # nodoc
|
228
228
|
original_pos = @data.pos
|
229
229
|
byte = @data.read(1)
|
@@ -231,10 +231,27 @@ module DBF
|
|
231
231
|
byte.ord == 13
|
232
232
|
end
|
233
233
|
|
234
|
+
def find_all(options) # nodoc
|
235
|
+
map do |record|
|
236
|
+
if record && record.match?(options)
|
237
|
+
yield record if block_given?
|
238
|
+
record
|
239
|
+
end
|
240
|
+
end.compact
|
241
|
+
end
|
242
|
+
|
243
|
+
def find_first(options) # nodoc
|
244
|
+
detect { |record| record && record.match?(options) }
|
245
|
+
end
|
246
|
+
|
234
247
|
def foxpro? # nodoc
|
235
248
|
FOXPRO_VERSIONS.keys.include? version
|
236
249
|
end
|
237
250
|
|
251
|
+
def header
|
252
|
+
@header ||= Header.new(@data.read DBF_HEADER_SIZE)
|
253
|
+
end
|
254
|
+
|
238
255
|
def memo_class # nodoc
|
239
256
|
@memo_class ||= begin
|
240
257
|
if foxpro?
|
@@ -245,8 +262,16 @@ module DBF
|
|
245
262
|
end
|
246
263
|
end
|
247
264
|
|
265
|
+
def memo_search_path(io) # nodoc
|
266
|
+
dirname = File.dirname(io)
|
267
|
+
basename = File.basename(io, '.*')
|
268
|
+
"#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}"
|
269
|
+
end
|
270
|
+
|
248
271
|
def open_data(data) # nodoc
|
249
272
|
data.is_a?(StringIO) ? data : File.open(data, 'rb')
|
273
|
+
rescue Errno::ENOENT
|
274
|
+
raise DBF::FileNotFoundError, "file not found: #{data}"
|
250
275
|
end
|
251
276
|
|
252
277
|
def open_memo(data, memo = nil) # nodoc
|
@@ -259,32 +284,8 @@ module DBF
|
|
259
284
|
end
|
260
285
|
end
|
261
286
|
|
262
|
-
def memo_search_path(io) # nodoc
|
263
|
-
dirname = File.dirname(io)
|
264
|
-
basename = File.basename(io, '.*')
|
265
|
-
"#{dirname}/#{basename}*.{fpt,FPT,dbt,DBT}"
|
266
|
-
end
|
267
|
-
|
268
|
-
def find_all(options) # nodoc
|
269
|
-
map do |record|
|
270
|
-
if record && record.match?(options)
|
271
|
-
yield record if block_given?
|
272
|
-
record
|
273
|
-
end
|
274
|
-
end.compact
|
275
|
-
end
|
276
|
-
|
277
|
-
def find_first(options) # nodoc
|
278
|
-
detect { |record| record && record.match?(options) }
|
279
|
-
end
|
280
|
-
|
281
|
-
def deleted_record? # nodoc
|
282
|
-
flag = @data.read(1)
|
283
|
-
flag ? flag.unpack('a') == ['*'] : true
|
284
|
-
end
|
285
|
-
|
286
287
|
def seek(offset) # nodoc
|
287
|
-
@data.seek
|
288
|
+
@data.seek(header.header_length + offset)
|
288
289
|
end
|
289
290
|
|
290
291
|
def seek_to_record(index) # nodoc
|
data/lib/dbf/version.rb
CHANGED
data/spec/dbf/column_spec.rb
CHANGED
@@ -60,13 +60,13 @@ RSpec.describe DBF::Column do
|
|
60
60
|
end
|
61
61
|
|
62
62
|
context 'and 0 decimals' do
|
63
|
-
it 'casts value to
|
63
|
+
it 'casts value to Integer' do
|
64
64
|
value = '135'
|
65
65
|
column = DBF::Column.new table, 'ColumnName', 'N', 3, 0
|
66
66
|
expect(column.type_cast(value)).to eq 135
|
67
67
|
end
|
68
68
|
|
69
|
-
it 'supports negative
|
69
|
+
it 'supports negative Integer' do
|
70
70
|
value = '-135'
|
71
71
|
column = DBF::Column.new table, 'ColumnName', 'N', 3, 0
|
72
72
|
expect(column.type_cast(value)).to eq (-135)
|
@@ -139,13 +139,13 @@ RSpec.describe DBF::Column do
|
|
139
139
|
end
|
140
140
|
end
|
141
141
|
|
142
|
-
it 'casts value to
|
142
|
+
it 'casts value to Integer' do
|
143
143
|
value = "\203\171\001\000"
|
144
144
|
column = DBF::Column.new table, 'ColumnName', 'I', 3, 0
|
145
145
|
expect(column.type_cast(value)).to eq 96643
|
146
146
|
end
|
147
147
|
|
148
|
-
it 'supports negative
|
148
|
+
it 'supports negative Integer' do
|
149
149
|
value = "\x24\xE1\xFF\xFF"
|
150
150
|
column = DBF::Column.new table, 'ColumnName', 'I', 3, 0
|
151
151
|
expect(column.type_cast(value)).to eq (-7900)
|
@@ -284,54 +284,6 @@ RSpec.describe DBF::Column do
|
|
284
284
|
end
|
285
285
|
end
|
286
286
|
|
287
|
-
context '#schema_definition' do
|
288
|
-
context 'with type N (number)' do
|
289
|
-
it 'outputs an integer column' do
|
290
|
-
column = DBF::Column.new table, 'ColumnName', 'N', 1, 0
|
291
|
-
expect(column.schema_definition).to eq "\"column_name\", :integer\n"
|
292
|
-
end
|
293
|
-
end
|
294
|
-
|
295
|
-
context 'with type B (binary)' do
|
296
|
-
context 'with Foxpro dbf' do
|
297
|
-
it 'outputs a float column' do
|
298
|
-
column = DBF::Column.new table, 'ColumnName', 'B', 1, 2
|
299
|
-
expect(column.schema_definition).to eq "\"column_name\", :binary\n"
|
300
|
-
end
|
301
|
-
end
|
302
|
-
end
|
303
|
-
|
304
|
-
it 'defines a float colmn if type is (N)umber with more than 0 decimals' do
|
305
|
-
column = DBF::Column.new table, 'ColumnName', 'N', 1, 2
|
306
|
-
expect(column.schema_definition).to eq "\"column_name\", :float\n"
|
307
|
-
end
|
308
|
-
|
309
|
-
it 'defines a date column if type is (D)ate' do
|
310
|
-
column = DBF::Column.new table, 'ColumnName', 'D', 8, 0
|
311
|
-
expect(column.schema_definition).to eq "\"column_name\", :date\n"
|
312
|
-
end
|
313
|
-
|
314
|
-
it 'defines a datetime column if type is (D)ate' do
|
315
|
-
column = DBF::Column.new table, 'ColumnName', 'T', 16, 0
|
316
|
-
expect(column.schema_definition).to eq "\"column_name\", :datetime\n"
|
317
|
-
end
|
318
|
-
|
319
|
-
it 'defines a boolean column if type is (L)ogical' do
|
320
|
-
column = DBF::Column.new table, 'ColumnName', 'L', 1, 0
|
321
|
-
expect(column.schema_definition).to eq "\"column_name\", :boolean\n"
|
322
|
-
end
|
323
|
-
|
324
|
-
it 'defines a text column if type is (M)emo' do
|
325
|
-
column = DBF::Column.new table, 'ColumnName', 'M', 1, 0
|
326
|
-
expect(column.schema_definition).to eq "\"column_name\", :text\n"
|
327
|
-
end
|
328
|
-
|
329
|
-
it 'defines a string column with length for any other data types' do
|
330
|
-
column = DBF::Column.new table, 'ColumnName', 'X', 20, 0
|
331
|
-
expect(column.schema_definition).to eq "\"column_name\", :string, :limit => 20\n"
|
332
|
-
end
|
333
|
-
end
|
334
|
-
|
335
287
|
context '#name' do
|
336
288
|
it 'contains only ASCII characters' do
|
337
289
|
column = DBF::Column.new table, "--\x1F-\x68\x65\x6C\x6C\x6F world-\x80--", 'N', 1, 0
|
@@ -31,9 +31,9 @@ RSpec.shared_examples_for 'DBF' do
|
|
31
31
|
end
|
32
32
|
end
|
33
33
|
|
34
|
-
specify "column lengths should be instances of
|
34
|
+
specify "column lengths should be instances of Integer" do
|
35
35
|
table.columns.each do |column|
|
36
|
-
expect(column.length).to be_kind_of(
|
36
|
+
expect(column.length).to be_kind_of(Integer)
|
37
37
|
end
|
38
38
|
end
|
39
39
|
|
@@ -43,9 +43,9 @@ RSpec.shared_examples_for 'DBF' do
|
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
46
|
-
specify "column decimals should be instances of
|
46
|
+
specify "column decimals should be instances of Integer" do
|
47
47
|
table.columns.each do |column|
|
48
|
-
expect(column.decimal).to be_kind_of(
|
48
|
+
expect(column.decimal).to be_kind_of(Integer)
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
data/spec/dbf/table_spec.rb
CHANGED
@@ -326,4 +326,52 @@ SCHEMA
|
|
326
326
|
expect(table.column_names).to eq column_names
|
327
327
|
end
|
328
328
|
end
|
329
|
+
|
330
|
+
context '#activerecord_schema_definition' do
|
331
|
+
context 'with type N (number)' do
|
332
|
+
it 'outputs an integer column' do
|
333
|
+
column = DBF::Column.new table, 'ColumnName', 'N', 1, 0
|
334
|
+
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :integer\n"
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
context 'with type B (binary)' do
|
339
|
+
context 'with Foxpro dbf' do
|
340
|
+
it 'outputs a float column' do
|
341
|
+
column = DBF::Column.new table, 'ColumnName', 'B', 1, 2
|
342
|
+
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :binary\n"
|
343
|
+
end
|
344
|
+
end
|
345
|
+
end
|
346
|
+
|
347
|
+
it 'defines a float colmn if type is (N)umber with more than 0 decimals' do
|
348
|
+
column = DBF::Column.new table, 'ColumnName', 'N', 1, 2
|
349
|
+
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :float\n"
|
350
|
+
end
|
351
|
+
|
352
|
+
it 'defines a date column if type is (D)ate' do
|
353
|
+
column = DBF::Column.new table, 'ColumnName', 'D', 8, 0
|
354
|
+
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :date\n"
|
355
|
+
end
|
356
|
+
|
357
|
+
it 'defines a datetime column if type is (D)ate' do
|
358
|
+
column = DBF::Column.new table, 'ColumnName', 'T', 16, 0
|
359
|
+
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :datetime\n"
|
360
|
+
end
|
361
|
+
|
362
|
+
it 'defines a boolean column if type is (L)ogical' do
|
363
|
+
column = DBF::Column.new table, 'ColumnName', 'L', 1, 0
|
364
|
+
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :boolean\n"
|
365
|
+
end
|
366
|
+
|
367
|
+
it 'defines a text column if type is (M)emo' do
|
368
|
+
column = DBF::Column.new table, 'ColumnName', 'M', 1, 0
|
369
|
+
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :text\n"
|
370
|
+
end
|
371
|
+
|
372
|
+
it 'defines a string column with length for any other data types' do
|
373
|
+
column = DBF::Column.new table, 'ColumnName', 'X', 20, 0
|
374
|
+
expect(table.activerecord_schema_definition(column)).to eq "\"column_name\", :string, :limit => 20\n"
|
375
|
+
end
|
376
|
+
end
|
329
377
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -4,7 +4,7 @@ begin
|
|
4
4
|
rescue LoadError
|
5
5
|
end
|
6
6
|
|
7
|
-
Encoding.default_external =
|
7
|
+
Encoding.default_external = 'UTF-8'
|
8
8
|
|
9
9
|
require 'dbf'
|
10
10
|
require 'yaml'
|
@@ -26,7 +26,7 @@ RSpec.configure do |config|
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def fixture_path
|
29
|
-
@fixture_path ||= File.join(File.dirname(__FILE__), '
|
29
|
+
@fixture_path ||= File.join(File.dirname(__FILE__), 'fixtures')
|
30
30
|
end
|
31
31
|
|
32
32
|
def fixture(filename)
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dbf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 3.1.
|
4
|
+
version: 3.1.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Keith Morrison
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2017-10-26 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: A small fast library for reading dBase, xBase, Clipper and FoxPro database
|
14
14
|
files.
|
@@ -110,13 +110,13 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
110
110
|
version: 1.3.0
|
111
111
|
requirements: []
|
112
112
|
rubyforge_project:
|
113
|
-
rubygems_version: 2.
|
113
|
+
rubygems_version: 2.6.11
|
114
114
|
signing_key:
|
115
115
|
specification_version: 4
|
116
116
|
summary: Read xBase files
|
117
117
|
test_files:
|
118
|
-
- spec/dbf/column_spec.rb
|
119
|
-
- spec/dbf/database_spec.rb
|
120
118
|
- spec/dbf/file_formats_spec.rb
|
119
|
+
- spec/dbf/database_spec.rb
|
120
|
+
- spec/dbf/column_spec.rb
|
121
121
|
- spec/dbf/record_spec.rb
|
122
122
|
- spec/dbf/table_spec.rb
|