dbf 1.0.11 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +5 -2
- data/README.markdown +37 -16
- data/Rakefile +7 -0
- data/VERSION.yml +2 -2
- data/dbf.gemspec +2 -2
- data/lib/dbf/column.rb +38 -1
- data/lib/dbf/record.rb +39 -6
- data/lib/dbf/table.rb +192 -140
- data/spec/functional/dbf_shared.rb +3 -1
- data/spec/spec_helper.rb +6 -5
- data/spec/unit/record_spec.rb +11 -11
- data/spec/unit/table_spec.rb +112 -80
- metadata +2 -2
data/History.txt
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
== 1.0.13
|
2
|
+
* Allow passing an array of ids to find
|
3
|
+
|
1
4
|
== 1.0.11
|
2
5
|
|
3
6
|
* Attributes are now accessible by original or underscored name
|
4
7
|
|
5
|
-
== 1.0
|
8
|
+
== 1.1.0
|
6
9
|
|
7
|
-
*
|
10
|
+
* Add support for large table that will not fit into memory
|
8
11
|
|
9
12
|
== 1.0.9
|
10
13
|
|
data/README.markdown
CHANGED
@@ -11,7 +11,9 @@ DBF is a small fast library for reading dBase, xBase, Clipper and FoxPro databas
|
|
11
11
|
|
12
12
|
* No external dependencies
|
13
13
|
* Fields are type cast to the appropriate Ruby types
|
14
|
+
* ActiveRecord like finder methods
|
14
15
|
* Ability to dump the database schema in the portable ActiveRecord::Schema format
|
16
|
+
* Ruby 1.9.1 compatible
|
15
17
|
|
16
18
|
## Installation
|
17
19
|
|
@@ -19,32 +21,51 @@ DBF is a small fast library for reading dBase, xBase, Clipper and FoxPro databas
|
|
19
21
|
|
20
22
|
## Basic Usage
|
21
23
|
|
24
|
+
Load a DBF file:
|
25
|
+
|
22
26
|
require 'rubygems'
|
23
27
|
require 'dbf'
|
24
28
|
|
25
29
|
table = DBF::Table.new("widgets.dbf")
|
26
30
|
|
27
|
-
|
28
|
-
widget_ids = table.map { |row| row.id }
|
29
|
-
abc_names = table.select { |row| row.name =~ /^[a-cA-C]/ }
|
30
|
-
sorted = table.sort_by { |row| row.name }
|
31
|
-
|
32
|
-
# Print the 'name' field from record number 4
|
33
|
-
puts table.record(4).name
|
31
|
+
Enumerate all records
|
34
32
|
|
35
|
-
# Attributes can also be accessed using the column name as a Hash key
|
36
|
-
puts table.record(4).attributes["name"]
|
37
|
-
|
38
|
-
# Print the 'name' and 'address' fields from each record
|
39
33
|
table.records.each do |record|
|
40
34
|
puts record.name
|
41
35
|
puts record.email
|
42
36
|
end
|
37
|
+
|
38
|
+
Load a single record using <tt>records</tt> or <tt>find</tt>
|
39
|
+
|
40
|
+
table.records(6)
|
41
|
+
table.find(6)
|
43
42
|
|
44
|
-
|
45
|
-
|
46
|
-
|
43
|
+
Attributes can also be accessed through the attributes hash in original or
|
44
|
+
underscored form or as an accessor method using the underscored name.
|
45
|
+
|
46
|
+
table.record(4).attributes["PhoneBook"]
|
47
|
+
table.record(4).attributes["phone_book"]
|
48
|
+
table.record(4).phone_book
|
49
|
+
|
50
|
+
Search for records using a simple hash format. Multiple search criteria are
|
51
|
+
ANDed. Use the block form of find if the resulting recordset could be large
|
52
|
+
otherwise all records will be loaded into memory.
|
53
|
+
|
54
|
+
# find all records with first_name equal to Keith
|
55
|
+
table.find(:all, :first_name => 'Keith') do |record|
|
56
|
+
puts record.last_name
|
57
|
+
end
|
58
|
+
|
59
|
+
# find all records with first_name equal to Keith and last_name equal
|
60
|
+
# to Morrison
|
61
|
+
table.find(:all, :first_name => 'Keith', :last_name => 'Morrison') do |record|
|
62
|
+
puts record.last_name
|
63
|
+
end
|
64
|
+
|
65
|
+
# find the first record with first_name equal to Keith
|
47
66
|
table.find :first, :first_name => 'Keith'
|
67
|
+
|
68
|
+
# find record number 10
|
48
69
|
table.find(10)
|
49
70
|
|
50
71
|
## Migrating to ActiveRecord
|
@@ -58,7 +79,7 @@ An example of migrating a DBF book table to ActiveRecord using a migration:
|
|
58
79
|
table = DBF::Table.new('db/dbf/books.dbf')
|
59
80
|
eval(table.schema)
|
60
81
|
|
61
|
-
table.
|
82
|
+
table.each do |record|
|
62
83
|
Book.create(record.attributes)
|
63
84
|
end
|
64
85
|
end
|
@@ -81,7 +102,7 @@ A small command-line utility called dbf is installed along with the gem.
|
|
81
102
|
## Limitations and known bugs
|
82
103
|
|
83
104
|
* DBF is read-only
|
84
|
-
*
|
105
|
+
* External index files are not used
|
85
106
|
|
86
107
|
## License
|
87
108
|
|
data/Rakefile
CHANGED
@@ -27,6 +27,13 @@ Spec::Rake::SpecTask.new :spec do |t|
|
|
27
27
|
t.spec_files = FileList['spec/**/*spec.rb']
|
28
28
|
end
|
29
29
|
|
30
|
+
desc "Run rcov"
|
31
|
+
Spec::Rake::SpecTask.new :rcov do |t|
|
32
|
+
t.spec_files = FileList['spec/**/*spec.rb']
|
33
|
+
t.rcov = true
|
34
|
+
t.rcov_opts = ["--exclude ~\/.gem"]
|
35
|
+
end
|
36
|
+
|
30
37
|
desc "Run spec docs"
|
31
38
|
Spec::Rake::SpecTask.new :specdoc do |t|
|
32
39
|
t.spec_opts = ["-f specdoc"]
|
data/VERSION.yml
CHANGED
data/dbf.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = %q{dbf}
|
8
|
-
s.version = "1.0
|
8
|
+
s.version = "1.1.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Keith Morrison"]
|
12
|
-
s.date = %q{2009-12-
|
12
|
+
s.date = %q{2009-12-14}
|
13
13
|
s.default_executable = %q{dbf}
|
14
14
|
s.description = %q{A small fast library for reading dBase, xBase, Clipper and FoxPro database files.}
|
15
15
|
s.email = %q{keithm@infused.org}
|
data/lib/dbf/column.rb
CHANGED
@@ -2,9 +2,17 @@ module DBF
|
|
2
2
|
class ColumnLengthError < DBFError; end
|
3
3
|
class ColumnNameError < DBFError; end
|
4
4
|
|
5
|
+
# DBF::Column stores all the information about a column including its name,
|
6
|
+
# type, length and number of decimal places (if any)
|
5
7
|
class Column
|
6
8
|
attr_reader :name, :type, :length, :decimal
|
7
9
|
|
10
|
+
# Initialize a new DBF::Column
|
11
|
+
#
|
12
|
+
# @param [String] name
|
13
|
+
# @param [String] type
|
14
|
+
# @param [Fixnum] length
|
15
|
+
# @param [Fixnum] decimal
|
8
16
|
def initialize(name, type, length, decimal)
|
9
17
|
@name, @type, @length, @decimal = strip_non_ascii_chars(name), type, length, decimal
|
10
18
|
|
@@ -12,6 +20,10 @@ module DBF
|
|
12
20
|
raise ColumnNameError, "column name cannot be empty" if @name.length == 0
|
13
21
|
end
|
14
22
|
|
23
|
+
# Cast value to native type
|
24
|
+
#
|
25
|
+
# @param [String] value
|
26
|
+
# @return [Fixnum, Float, Date, DateTime, Boolean, String]
|
15
27
|
def type_cast(value)
|
16
28
|
case type
|
17
29
|
when 'N' # number
|
@@ -29,28 +41,50 @@ module DBF
|
|
29
41
|
end
|
30
42
|
end
|
31
43
|
|
44
|
+
# Decode a DateTime value
|
45
|
+
#
|
46
|
+
# @param [String] value
|
47
|
+
# @return [DateTime]
|
32
48
|
def decode_datetime(value)
|
33
49
|
days, milliseconds = value.unpack('l2')
|
34
50
|
seconds = milliseconds / 1000
|
35
51
|
DateTime.jd(days, seconds/3600, seconds/60 % 60, seconds % 60)
|
36
52
|
end
|
37
53
|
|
54
|
+
# Decode a number value
|
55
|
+
#
|
56
|
+
# @param [String] value
|
57
|
+
# @return [Fixnum, Float]
|
38
58
|
def unpack_number(value)
|
39
59
|
decimal.zero? ? unpack_integer(value) : value.to_f
|
40
60
|
end
|
41
61
|
|
62
|
+
# Decode an integer
|
63
|
+
#
|
64
|
+
# @param [String] value
|
65
|
+
# @return [Fixnum]
|
42
66
|
def unpack_integer(value)
|
43
67
|
value.to_i
|
44
68
|
end
|
45
69
|
|
70
|
+
# Decode a boolean value
|
71
|
+
#
|
72
|
+
# @param [String] value
|
73
|
+
# @return [Boolean]
|
46
74
|
def boolean(value)
|
47
75
|
value.strip =~ /^(y|t)$/i ? true : false
|
48
76
|
end
|
49
77
|
|
78
|
+
# Schema definition
|
79
|
+
#
|
80
|
+
# @return [String]
|
50
81
|
def schema_definition
|
51
82
|
"\"#{name.underscore}\", #{schema_data_type}\n"
|
52
83
|
end
|
53
84
|
|
85
|
+
# Column type for schema definition
|
86
|
+
#
|
87
|
+
# @return [String]
|
54
88
|
def schema_data_type
|
55
89
|
case type
|
56
90
|
when "N"
|
@@ -70,7 +104,10 @@ module DBF
|
|
70
104
|
end
|
71
105
|
end
|
72
106
|
|
73
|
-
#
|
107
|
+
# Strip all non-ascii and non-printable characters
|
108
|
+
#
|
109
|
+
# @param [String] s
|
110
|
+
# @return [String]
|
74
111
|
def strip_non_ascii_chars(s)
|
75
112
|
# truncate the string at the first null character
|
76
113
|
s = s[0, s.index("\x00")] if s.index("\x00")
|
data/lib/dbf/record.rb
CHANGED
@@ -1,24 +1,40 @@
|
|
1
1
|
module DBF
|
2
|
+
|
3
|
+
# An instance of DBF::Record represents a row in the DBF file
|
2
4
|
class Record
|
3
5
|
attr_reader :attributes
|
6
|
+
attr_reader :memo_block_size
|
7
|
+
|
4
8
|
delegate :columns, :to => :@table
|
5
9
|
|
10
|
+
# Initialize a new DBF::Record
|
11
|
+
#
|
12
|
+
# @param [DBF::Table] table
|
6
13
|
def initialize(table)
|
7
14
|
@table, @data, @memo = table, table.data, table.memo
|
15
|
+
@memo_block_size = @table.memo_block_size
|
8
16
|
initialize_values
|
9
17
|
define_accessors
|
10
18
|
end
|
11
19
|
|
20
|
+
# Equality
|
21
|
+
#
|
22
|
+
# @param [DBF::Record] other
|
23
|
+
# @return [Boolean]
|
12
24
|
def ==(other)
|
13
25
|
other.respond_to?(:attributes) && other.attributes == attributes
|
14
26
|
end
|
15
27
|
|
28
|
+
# Maps a row to an array of values
|
29
|
+
#
|
30
|
+
# @return [Array]
|
16
31
|
def to_a
|
17
32
|
columns.map { |column| @attributes[column.name.underscore] }
|
18
33
|
end
|
19
34
|
|
20
35
|
private
|
21
36
|
|
37
|
+
# Defined attribute accessor methods
|
22
38
|
def define_accessors
|
23
39
|
columns.each do |column|
|
24
40
|
underscored_column_name = column.name.underscore
|
@@ -30,6 +46,7 @@ module DBF
|
|
30
46
|
end
|
31
47
|
end
|
32
48
|
|
49
|
+
# Initialize values for a row
|
33
50
|
def initialize_values
|
34
51
|
@attributes = columns.inject({}) do |hash, column|
|
35
52
|
if column.type == 'M'
|
@@ -44,17 +61,27 @@ module DBF
|
|
44
61
|
hash
|
45
62
|
end
|
46
63
|
end
|
47
|
-
|
64
|
+
|
65
|
+
# Unpack raw data from database
|
66
|
+
#
|
67
|
+
# @param [Fixnum] length
|
48
68
|
def unpack_data(length)
|
49
69
|
@data.read(length).unpack("a#{length}").first
|
50
70
|
end
|
51
|
-
|
71
|
+
|
72
|
+
# Reads a memo from the memo file
|
73
|
+
#
|
74
|
+
# @param [Fixnum] start_block
|
52
75
|
def read_memo(start_block)
|
53
76
|
return nil if !@table.has_memo_file? || start_block < 1
|
54
77
|
|
55
78
|
@table.memo_file_format == :fpt ? build_fpt_memo(start_block) : build_dbt_memo(start_block)
|
56
79
|
end
|
57
80
|
|
81
|
+
# Reconstructs a memo from an FPT memo file
|
82
|
+
#
|
83
|
+
# @param [Fixnum] start_block
|
84
|
+
# @return [String]
|
58
85
|
def build_fpt_memo(start_block)
|
59
86
|
@memo.seek(start_block * memo_block_size)
|
60
87
|
|
@@ -69,6 +96,10 @@ module DBF
|
|
69
96
|
memo_string
|
70
97
|
end
|
71
98
|
|
99
|
+
# Reconstucts a memo from an DBT memo file
|
100
|
+
#
|
101
|
+
# @param [Fixnum] start_block
|
102
|
+
# @return [String]
|
72
103
|
def build_dbt_memo(start_block)
|
73
104
|
@memo.seek(start_block * memo_block_size)
|
74
105
|
|
@@ -87,14 +118,16 @@ module DBF
|
|
87
118
|
memo_string
|
88
119
|
end
|
89
120
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
121
|
+
# The size in bytes of the content for each memo block
|
122
|
+
#
|
123
|
+
# @return [Fixnum]
|
94
124
|
def memo_block_content_size
|
95
125
|
memo_block_size - BLOCK_HEADER_SIZE
|
96
126
|
end
|
97
127
|
|
128
|
+
# The size in bytes of the entire memo
|
129
|
+
#
|
130
|
+
# @return [Fixnum]
|
98
131
|
def memo_content_size(memo_size)
|
99
132
|
(memo_size - memo_block_size) + BLOCK_HEADER_SIZE
|
100
133
|
end
|
data/lib/dbf/table.rb
CHANGED
@@ -1,120 +1,93 @@
|
|
1
1
|
module DBF
|
2
2
|
|
3
|
+
# DBF::Table is the primary interface to a single DBF file and provides
|
4
|
+
# methods for enumerating and searching the records.
|
3
5
|
class Table
|
4
|
-
include Enumerable
|
5
|
-
|
6
6
|
attr_reader :column_count # The total number of columns
|
7
7
|
attr_reader :columns # An array of DBF::Column
|
8
8
|
attr_reader :version # Internal dBase version number
|
9
9
|
attr_reader :last_updated # Last updated datetime
|
10
10
|
attr_reader :memo_file_format # :fpt or :dpt
|
11
11
|
attr_reader :memo_block_size # The block size for memo records
|
12
|
-
attr_reader :options # The options hash
|
12
|
+
attr_reader :options # The options hash used to initialize the table
|
13
13
|
attr_reader :data # DBF file handle
|
14
14
|
attr_reader :memo # Memo file handle
|
15
|
+
attr_reader :record_count # Total number of records
|
15
16
|
|
16
|
-
#
|
17
|
+
# Opens a DBF::Table
|
17
18
|
# Example:
|
18
19
|
# table = DBF::Table.new 'data.dbf'
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
@
|
20
|
+
#
|
21
|
+
# @param [String] path Path to the dbf file
|
22
|
+
def initialize(path)
|
23
|
+
@data = File.open(path, 'rb')
|
24
|
+
@memo = open_memo(path)
|
23
25
|
reload!
|
24
26
|
end
|
25
27
|
|
28
|
+
# Closes the table and memo file
|
29
|
+
def close
|
30
|
+
@data.close
|
31
|
+
@memo.close if @memo
|
32
|
+
end
|
33
|
+
|
26
34
|
# Reloads the database and memo files
|
27
35
|
def reload!
|
28
36
|
@records = nil
|
29
37
|
get_header_info
|
30
38
|
get_memo_header_info if @memo
|
31
39
|
get_column_descriptors
|
32
|
-
build_db_index
|
33
40
|
end
|
34
41
|
|
35
|
-
#
|
42
|
+
# Checks if there is a memo file
|
43
|
+
#
|
44
|
+
# @return [Boolean]
|
36
45
|
def has_memo_file?
|
37
46
|
@memo ? true : false
|
38
47
|
end
|
39
48
|
|
40
|
-
#
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
# Returns an instance of DBF::Column for <b>column_name</b>. The <b>column_name</b>
|
46
|
-
# can be a specified as either a symbol or string.
|
49
|
+
# Retrieve a Column by name
|
50
|
+
#
|
51
|
+
# @param [String, Symbol] column_name
|
52
|
+
# @return [DBF::Column]
|
47
53
|
def column(column_name)
|
48
54
|
@columns.detect {|f| f.name == column_name.to_s}
|
49
55
|
end
|
50
56
|
|
51
|
-
#
|
52
|
-
#
|
53
|
-
|
54
|
-
|
55
|
-
end
|
56
|
-
|
57
|
-
alias_method :rows, :records
|
58
|
-
|
57
|
+
# Calls block once for each record in the table. The record may be nil
|
58
|
+
# if the record has been marked as deleted.
|
59
|
+
#
|
60
|
+
# @yield [nil, DBF::Record]
|
59
61
|
def each
|
60
62
|
0.upto(@record_count - 1) do |n|
|
61
63
|
seek_to_record(n)
|
62
|
-
|
63
|
-
yield DBF::Record.new(self)
|
64
|
-
end
|
64
|
+
yield deleted_record? ? nil : DBF::Record.new(self)
|
65
65
|
end
|
66
66
|
end
|
67
67
|
|
68
|
-
#
|
69
|
-
def record(index)
|
70
|
-
records[index]
|
71
|
-
end
|
72
|
-
|
73
|
-
# Find records using a simple ActiveRecord-like syntax.
|
74
|
-
#
|
75
|
-
# Examples:
|
76
|
-
# table = DBF::Table.new 'mydata.dbf'
|
77
|
-
#
|
78
|
-
# # Find record number 5
|
79
|
-
# table.find(5)
|
80
|
-
#
|
81
|
-
# # Find all records for Keith Morrison
|
82
|
-
# table.find :all, :first_name => "Keith", :last_name => "Morrison"
|
83
|
-
#
|
84
|
-
# # Find first record
|
85
|
-
# table.find :first, :first_name => "Keith"
|
68
|
+
# Retrieve a record by index number
|
86
69
|
#
|
87
|
-
#
|
88
|
-
#
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
# AND key2 = 'value2'".
|
93
|
-
def find(command, options = {})
|
94
|
-
results = options.empty? ? records : records.select {|record| all_values_match?(record, options)}
|
95
|
-
|
96
|
-
case command
|
97
|
-
when Fixnum
|
98
|
-
record(command)
|
99
|
-
when :all
|
100
|
-
results
|
101
|
-
when :first
|
102
|
-
results.first
|
103
|
-
end
|
70
|
+
# @param [Fixnum] index
|
71
|
+
# @return [DBF::Record]
|
72
|
+
def record(index)
|
73
|
+
seek_to_record(index)
|
74
|
+
deleted_record? ? nil : DBF::Record.new(self)
|
104
75
|
end
|
105
76
|
|
106
77
|
alias_method :row, :record
|
107
78
|
|
108
|
-
#
|
79
|
+
# Human readable version description
|
80
|
+
#
|
81
|
+
# @return [String]
|
109
82
|
def version_description
|
110
83
|
VERSION_DESCRIPTIONS[version]
|
111
84
|
end
|
112
85
|
|
113
|
-
#
|
86
|
+
# Generate an ActiveRecord::Schema
|
114
87
|
#
|
115
88
|
# xBase data types are converted to generic types as follows:
|
116
|
-
# - Number columns are converted to :integer
|
117
|
-
#
|
89
|
+
# - Number columns with no decimals are converted to :integer
|
90
|
+
# - Number columns with decimals are converted to :float
|
118
91
|
# - Date columns are converted to :datetime
|
119
92
|
# - Logical columns are converted to :boolean
|
120
93
|
# - Memo columns are converted to :text
|
@@ -129,6 +102,9 @@ module DBF
|
|
129
102
|
# t.column :age, :integer
|
130
103
|
# t.column :notes, :text
|
131
104
|
# end
|
105
|
+
#
|
106
|
+
# @param [optional String] path
|
107
|
+
# @return [String]
|
132
108
|
def schema(path = nil)
|
133
109
|
s = "ActiveRecord::Schema.define do\n"
|
134
110
|
s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
|
@@ -139,105 +115,181 @@ module DBF
|
|
139
115
|
|
140
116
|
if path
|
141
117
|
File.open(path, 'w') {|f| f.puts(s)}
|
142
|
-
else
|
143
|
-
s
|
144
118
|
end
|
119
|
+
|
120
|
+
s
|
145
121
|
end
|
146
122
|
|
147
|
-
#
|
148
|
-
#
|
149
|
-
#
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
# Dumps all records into a CSV file
|
156
|
-
def to_csv(filename = nil)
|
157
|
-
filename = File.basename(@data.path, '.dbf') + '.csv' if filename.nil?
|
158
|
-
FCSV.open(filename, 'w', :force_quotes => true) do |csv|
|
159
|
-
records.each do |record|
|
123
|
+
# Dumps all records to a CSV file. If no filename is given then CSV is
|
124
|
+
# output to STDOUT.
|
125
|
+
#
|
126
|
+
# @param [optional String] path Defaults to basename of dbf file
|
127
|
+
def to_csv(path = nil)
|
128
|
+
path = File.basename(@data.path, '.dbf') + '.csv' if path.nil?
|
129
|
+
FCSV.open(path, 'w', :force_quotes => true) do |csv|
|
130
|
+
each do |record|
|
160
131
|
csv << record.to_a
|
161
132
|
end
|
162
133
|
end
|
163
134
|
end
|
164
135
|
|
136
|
+
# Find records using a simple ActiveRecord-like syntax.
|
137
|
+
#
|
138
|
+
# Examples:
|
139
|
+
# table = DBF::Table.new 'mydata.dbf'
|
140
|
+
#
|
141
|
+
# # Find record number 5
|
142
|
+
# table.find(5)
|
143
|
+
#
|
144
|
+
# # Find all records for Keith Morrison
|
145
|
+
# table.find :all, :first_name => "Keith", :last_name => "Morrison"
|
146
|
+
#
|
147
|
+
# # Find first record
|
148
|
+
# table.find :first, :first_name => "Keith"
|
149
|
+
#
|
150
|
+
# The <b>command</b> may be a record index, :all, or :first.
|
151
|
+
# <b>options</b> is optional and, if specified, should be a hash where the keys correspond
|
152
|
+
# to column names in the database. The values will be matched exactly with the value
|
153
|
+
# in the database. If you specify more than one key, all values must match in order
|
154
|
+
# for the record to be returned. The equivalent SQL would be "WHERE key1 = 'value1'
|
155
|
+
# AND key2 = 'value2'".
|
156
|
+
#
|
157
|
+
# @param [Fixnum, Symbol] command
|
158
|
+
# @param [optional, Hash] options Hash of search parameters
|
159
|
+
# @yield [optional, DBF::Record]
|
160
|
+
def find(command, options = {}, &block)
|
161
|
+
case command
|
162
|
+
when Fixnum
|
163
|
+
record(command)
|
164
|
+
when Array
|
165
|
+
command.map {|i| record(i)}
|
166
|
+
when :all
|
167
|
+
find_all(options, &block)
|
168
|
+
when :first
|
169
|
+
find_first(options)
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
165
173
|
private
|
166
174
|
|
167
|
-
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
175
|
+
# Find all matching
|
176
|
+
#
|
177
|
+
# @param [Hash] options
|
178
|
+
# @yield [optional DBF::Record]
|
179
|
+
# @return [Array]
|
180
|
+
def find_all(options, &block)
|
181
|
+
results = []
|
182
|
+
each do |record|
|
183
|
+
if all_values_match?(record, options)
|
184
|
+
if block_given?
|
185
|
+
yield(record)
|
186
|
+
else
|
187
|
+
results << record
|
173
188
|
end
|
174
189
|
end
|
175
|
-
nil
|
176
|
-
end
|
177
|
-
|
178
|
-
def replace_extname(filename, extension)
|
179
|
-
filename.sub(/#{File.extname(filename)[1..-1]}$/, extension)
|
180
190
|
end
|
191
|
+
results
|
192
|
+
end
|
181
193
|
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
194
|
+
# Find first matching
|
195
|
+
#
|
196
|
+
# @param [Hash] options
|
197
|
+
# @return [DBF::Record, nil]
|
198
|
+
def find_first(options)
|
199
|
+
each do |record|
|
200
|
+
return record if all_values_match?(record, options)
|
189
201
|
end
|
202
|
+
nil
|
203
|
+
end
|
190
204
|
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
205
|
+
# Do all search parameters match?
|
206
|
+
#
|
207
|
+
# @param [DBF::Record] record
|
208
|
+
# @param [Hash] options
|
209
|
+
# @return [Boolean]
|
210
|
+
def all_values_match?(record, options)
|
211
|
+
options.all? {|key, value| record.attributes[key.to_s.underscore] == value}
|
212
|
+
end
|
196
213
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
214
|
+
# Open memo file
|
215
|
+
#
|
216
|
+
# @params [String] path
|
217
|
+
# @return [File]
|
218
|
+
def open_memo(path)
|
219
|
+
%w(fpt FPT dbt DBT).each do |extname|
|
220
|
+
filename = replace_extname(path, extname)
|
221
|
+
if File.exists?(filename)
|
222
|
+
@memo_file_format = extname.downcase.to_sym
|
223
|
+
return File.open(filename, 'rb')
|
204
224
|
end
|
205
|
-
# Reset the column count in case any were skipped
|
206
|
-
@column_count = @columns.size
|
207
|
-
|
208
|
-
@columns
|
209
225
|
end
|
226
|
+
nil
|
227
|
+
end
|
210
228
|
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
end
|
220
|
-
end
|
229
|
+
# Replace the file extension
|
230
|
+
#
|
231
|
+
# @param [String] path
|
232
|
+
# @param [String] extension
|
233
|
+
# @return [String]
|
234
|
+
def replace_extname(path, extension)
|
235
|
+
path.sub(/#{File.extname(path)[1..-1]}$/, extension)
|
236
|
+
end
|
221
237
|
|
222
|
-
|
223
|
-
|
224
|
-
|
238
|
+
# Is record marked for deletion
|
239
|
+
#
|
240
|
+
# @return [Boolean]
|
241
|
+
def deleted_record?
|
242
|
+
@data.read(1).unpack('a') == ['*']
|
243
|
+
end
|
225
244
|
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
245
|
+
# Determine database version, record count, header length and record length
|
246
|
+
def get_header_info
|
247
|
+
@data.rewind
|
248
|
+
@version, @record_count, @header_length, @record_length = @data.read(DBF_HEADER_SIZE).unpack('H2 x3 V v2')
|
249
|
+
@column_count = (@header_length - DBF_HEADER_SIZE + 1) / DBF_HEADER_SIZE
|
250
|
+
end
|
251
|
+
|
252
|
+
# Retrieves column information from the database
|
253
|
+
def get_column_descriptors
|
254
|
+
@columns = []
|
255
|
+
@column_count.times do
|
256
|
+
name, type, length, decimal = @data.read(32).unpack('a10 x a x4 C2')
|
257
|
+
if length > 0
|
258
|
+
@columns << Column.new(name.strip, type, length, decimal)
|
235
259
|
end
|
236
260
|
end
|
261
|
+
# Reset the column count in case any were skipped
|
262
|
+
@column_count = @columns.size
|
237
263
|
|
238
|
-
|
239
|
-
|
264
|
+
@columns
|
265
|
+
end
|
266
|
+
|
267
|
+
# Determines the memo block size and next available block
|
268
|
+
def get_memo_header_info
|
269
|
+
@memo.rewind
|
270
|
+
if @memo_file_format == :fpt
|
271
|
+
@memo_next_available_block, @memo_block_size = @memo.read(FPT_HEADER_SIZE).unpack('N x2 n')
|
272
|
+
@memo_block_size = 0 if @memo_block_size.nil?
|
273
|
+
else
|
274
|
+
@memo_block_size = 512
|
275
|
+
@memo_next_available_block = File.size(@memo.path) / @memo_block_size
|
240
276
|
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Seek to a byte offset
|
280
|
+
#
|
281
|
+
# @params [Fixnum] offset
|
282
|
+
def seek(offset)
|
283
|
+
@data.seek(@header_length + offset)
|
284
|
+
end
|
285
|
+
|
286
|
+
# Seek to a record
|
287
|
+
#
|
288
|
+
# @param [Fixnum] index
|
289
|
+
def seek_to_record(index)
|
290
|
+
seek(index * @record_length)
|
291
|
+
end
|
292
|
+
|
241
293
|
end
|
242
294
|
|
243
295
|
end
|
@@ -7,7 +7,9 @@ describe DBF, :shared => true do
|
|
7
7
|
end
|
8
8
|
|
9
9
|
specify "records should be instances of DBF::Record" do
|
10
|
-
@table.
|
10
|
+
@table.each do |record|
|
11
|
+
record.should be_an_instance_of(DBF::Record)
|
12
|
+
end
|
11
13
|
end
|
12
14
|
|
13
15
|
specify "columns should be instances of DBF::Column" do
|
data/spec/spec_helper.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
|
-
$:.unshift(File.dirname(__FILE__) +
|
2
|
-
require
|
3
|
-
require
|
4
|
-
require
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib/')
|
2
|
+
require 'rubygems'
|
3
|
+
require 'spec'
|
4
|
+
require 'dbf'
|
5
|
+
require 'fileutils'
|
5
6
|
|
6
7
|
DB_PATH = File.dirname(__FILE__) + '/fixtures' unless defined?(DB_PATH)
|
7
8
|
|
@@ -9,4 +10,4 @@ Spec::Runner.configure do |config|
|
|
9
10
|
|
10
11
|
end
|
11
12
|
|
12
|
-
self.class.send :remove_const,
|
13
|
+
self.class.send :remove_const, 'Test' if defined? Test
|
data/spec/unit/record_spec.rb
CHANGED
@@ -25,26 +25,26 @@ describe DBF::Record do
|
|
25
25
|
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
26
26
|
table.column("ID").type.should == "N"
|
27
27
|
table.column("ID").decimal.should == 0
|
28
|
-
table.
|
28
|
+
table.record(1).attributes['id'].should be_kind_of(Integer)
|
29
29
|
end
|
30
30
|
|
31
31
|
it "should typecast number columns with decimals > 0 to Float" do
|
32
32
|
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
33
33
|
table.column("ID").type.should == "N"
|
34
34
|
table.column("COST").decimal.should == 2
|
35
|
-
table.
|
35
|
+
table.record(1).attributes['cost'].should be_kind_of(Float)
|
36
36
|
end
|
37
37
|
|
38
38
|
it "should typecast memo columns to String" do
|
39
39
|
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
40
40
|
table.column("DESC").type.should == "M"
|
41
|
-
table.
|
41
|
+
table.record(1).attributes['desc'].should be_kind_of(String)
|
42
42
|
end
|
43
43
|
|
44
44
|
it "should typecast logical columns to True or False" do
|
45
45
|
table = DBF::Table.new "#{DB_PATH}/dbase_30.dbf"
|
46
46
|
table.column("WEBINCLUDE").type.should == "L"
|
47
|
-
table.
|
47
|
+
table.record(1).attributes["webinclude"].should satisfy {|v| v == true || v == false}
|
48
48
|
end
|
49
49
|
end
|
50
50
|
|
@@ -85,13 +85,13 @@ describe DBF::Record do
|
|
85
85
|
end
|
86
86
|
end
|
87
87
|
|
88
|
-
describe '#to_a' do
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
end
|
88
|
+
# describe '#to_a' do
|
89
|
+
# it 'should return an ordered array of attribute values' do
|
90
|
+
# table = DBF::Table.new "#{DB_PATH}/dbase_8b.dbf"
|
91
|
+
# record = table.records[9]
|
92
|
+
# record.to_a.should == ["Ten records stored in this database", 10.0, nil, false, "0.100000000000000000", nil]
|
93
|
+
# end
|
94
|
+
# end
|
95
95
|
|
96
96
|
describe '#==' do
|
97
97
|
before do
|
data/spec/unit/table_spec.rb
CHANGED
@@ -1,8 +1,6 @@
|
|
1
1
|
require File.dirname(__FILE__) + "/../spec_helper"
|
2
|
-
require 'fileutils'
|
3
2
|
|
4
3
|
describe DBF::Table do
|
5
|
-
|
6
4
|
context "when initialized" do
|
7
5
|
before do
|
8
6
|
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
@@ -12,7 +10,7 @@ describe DBF::Table do
|
|
12
10
|
@table.data.should be_kind_of(File)
|
13
11
|
end
|
14
12
|
|
15
|
-
it "should
|
13
|
+
it "should load the memo file" do
|
16
14
|
@table.has_memo_file?.should be_true
|
17
15
|
@table.instance_eval("@memo").should be_kind_of(File)
|
18
16
|
end
|
@@ -36,74 +34,19 @@ describe DBF::Table do
|
|
36
34
|
it "should determine the database version" do
|
37
35
|
@table.version.should == "83"
|
38
36
|
end
|
39
|
-
|
40
37
|
end
|
41
38
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
46
|
-
table.find(5).should == table.record(5)
|
47
|
-
end
|
48
|
-
end
|
49
|
-
|
50
|
-
describe "with :all" do
|
51
|
-
before do
|
52
|
-
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
53
|
-
end
|
54
|
-
|
55
|
-
it "should return all records if options are empty" do
|
56
|
-
@table.find(:all).should == @table.records
|
57
|
-
end
|
58
|
-
|
59
|
-
it "should return matching records when used with options" do
|
60
|
-
@table.find(:all, "WEIGHT" => 0.0).should == @table.select {|r| r.attributes["weight"] == 0.0}
|
61
|
-
end
|
62
|
-
|
63
|
-
it "with multiple options should search for all search terms as if using AND" do
|
64
|
-
@table.find(:all, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should == []
|
65
|
-
end
|
66
|
-
|
67
|
-
it "should match original column names" do
|
68
|
-
@table.find(:all, "WEIGHT" => 0.0).should_not be_empty
|
69
|
-
end
|
70
|
-
|
71
|
-
it "should match symbolized column names" do
|
72
|
-
@table.find(:all, :WEIGHT => 0.0).should_not be_empty
|
73
|
-
end
|
74
|
-
|
75
|
-
it "should match downcased column names" do
|
76
|
-
@table.find(:all, "weight" => 0.0).should_not be_empty
|
77
|
-
end
|
78
|
-
|
79
|
-
it "should match symbolized downcased column names" do
|
80
|
-
@table.find(:all, :weight => 0.0).should_not be_empty
|
81
|
-
end
|
39
|
+
context "when closed" do
|
40
|
+
before do
|
41
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
82
42
|
end
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
end
|
88
|
-
|
89
|
-
it "should return the first record if options are empty" do
|
90
|
-
@table.find(:first).should == @table.records.first
|
91
|
-
end
|
92
|
-
|
93
|
-
it "should return the first matching record when used with options" do
|
94
|
-
@table.find(:first, "CODE" => "C").should == @table.record(5)
|
95
|
-
end
|
96
|
-
|
97
|
-
it "with multiple options should search for all search terms as if using AND" do
|
98
|
-
@table.find(:first, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should be_nil
|
99
|
-
end
|
43
|
+
|
44
|
+
it "should close the data file" do
|
45
|
+
@table.close
|
46
|
+
lambda { @table.record(1) }.should raise_error(IOError)
|
100
47
|
end
|
101
48
|
end
|
102
49
|
|
103
|
-
describe "#reload" do
|
104
|
-
# TODO
|
105
|
-
end
|
106
|
-
|
107
50
|
describe "#column" do
|
108
51
|
it "should accept a string or symbol as input" do
|
109
52
|
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
@@ -139,22 +82,9 @@ describe DBF::Table do
|
|
139
82
|
end
|
140
83
|
|
141
84
|
describe '#replace_extname' do
|
142
|
-
it
|
85
|
+
it "should change the file extension" do
|
143
86
|
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
144
|
-
table.send(:replace_extname,
|
145
|
-
end
|
146
|
-
end
|
147
|
-
|
148
|
-
describe '#each' do
|
149
|
-
it 'should enumerate all records' do
|
150
|
-
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
151
|
-
records = []
|
152
|
-
table.each do |record|
|
153
|
-
records << record
|
154
|
-
end
|
155
|
-
|
156
|
-
records.map! { |r| r.attributes }
|
157
|
-
records.should == table.records.map {|r| r.attributes}
|
87
|
+
table.send(:replace_extname, 'dbase_83.dbf', 'fpt').should == 'dbase_83.fpt'
|
158
88
|
end
|
159
89
|
end
|
160
90
|
|
@@ -178,6 +108,108 @@ describe DBF::Table do
|
|
178
108
|
File.exists?('test.csv').should be_true
|
179
109
|
end
|
180
110
|
end
|
111
|
+
|
112
|
+
describe "#record" do
|
113
|
+
before do
|
114
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
115
|
+
|
116
|
+
@records = []
|
117
|
+
@table.each {|record| @records << record}
|
118
|
+
end
|
119
|
+
|
120
|
+
it "should nullify deleted records" do
|
121
|
+
@table.stub!(:deleted_record?).and_return(true)
|
122
|
+
@table.record(5).should be_nil
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "#find" do
|
127
|
+
describe "with index" do
|
128
|
+
before do
|
129
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should return the correct record" do
|
133
|
+
@table.find(5).should == @table.record(5)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
describe 'with array of indexes' do
|
138
|
+
before do
|
139
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
140
|
+
end
|
141
|
+
|
142
|
+
it "should return the correct records" do
|
143
|
+
@table.find([1, 5, 10]).should == [@table.record(1), @table.record(5), @table.record(10)]
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
describe "with :all" do
|
148
|
+
before do
|
149
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
150
|
+
|
151
|
+
@records = []
|
152
|
+
@table.each {|record| @records << record}
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should accept a block" do
|
156
|
+
records = []
|
157
|
+
@table.find(:all, :weight => 0.0) do |record|
|
158
|
+
records << record
|
159
|
+
end
|
160
|
+
records.should == @table.find(:all, :weight => 0.0)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "should return all records if options are empty" do
|
164
|
+
@table.find(:all).should == @records
|
165
|
+
end
|
166
|
+
|
167
|
+
it "should return matching records when used with options" do
|
168
|
+
@table.find(:all, "WEIGHT" => 0.0).should == @records.select {|r| r.attributes["weight"] == 0.0}
|
169
|
+
end
|
170
|
+
|
171
|
+
it "should AND multiple search terms" do
|
172
|
+
@table.find(:all, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should == []
|
173
|
+
end
|
174
|
+
|
175
|
+
it "should match original column names" do
|
176
|
+
@table.find(:all, "WEIGHT" => 0.0).should_not be_empty
|
177
|
+
end
|
178
|
+
|
179
|
+
it "should match symbolized column names" do
|
180
|
+
@table.find(:all, :WEIGHT => 0.0).should_not be_empty
|
181
|
+
end
|
182
|
+
|
183
|
+
it "should match downcased column names" do
|
184
|
+
@table.find(:all, "weight" => 0.0).should_not be_empty
|
185
|
+
end
|
186
|
+
|
187
|
+
it "should match symbolized downcased column names" do
|
188
|
+
@table.find(:all, :weight => 0.0).should_not be_empty
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
describe "with :first" do
|
193
|
+
before do
|
194
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
195
|
+
|
196
|
+
@records = []
|
197
|
+
@table.each {|record| @records << record}
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should return the first record if options are empty" do
|
201
|
+
@table.find(:first).should == @records.first
|
202
|
+
end
|
203
|
+
|
204
|
+
it "should return the first matching record when used with options" do
|
205
|
+
@table.find(:first, "CODE" => "C").should == @records[5]
|
206
|
+
end
|
207
|
+
|
208
|
+
it "should AND multiple search terms" do
|
209
|
+
@table.find(:first, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should be_nil
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
181
213
|
|
182
214
|
end
|
183
215
|
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: dbf
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.0
|
4
|
+
version: 1.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Keith Morrison
|
@@ -9,7 +9,7 @@ autorequire:
|
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
11
|
|
12
|
-
date: 2009-12-
|
12
|
+
date: 2009-12-14 00:00:00 +01:00
|
13
13
|
default_executable: dbf
|
14
14
|
dependencies:
|
15
15
|
- !ruby/object:Gem::Dependency
|