dbf 3.1.0 → 3.1.1
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 +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
|