ruby-adt 0.5.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/README.markdown +123 -0
- data/Rakefile +32 -0
- data/VERSION +1 -0
- data/adt_format_notes.txt +36 -0
- data/lib/adt.rb +22 -0
- data/lib/adt/column.rb +91 -0
- data/lib/adt/globals.rb +13 -0
- data/lib/adt/record.rb +65 -0
- data/lib/adt/table.rb +259 -0
- data/spec/fixtures/A.ADT +0 -0
- data/spec/fixtures/test.adt +0 -0
- data/spec/functional/adt_shared.rb +54 -0
- data/spec/spec_helper.rb +13 -0
- data/spec/unit/column_spec.rb +137 -0
- data/spec/unit/record_spec.rb +124 -0
- data/spec/unit/table_spec.rb +227 -0
- metadata +94 -0
data/README.markdown
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# NOTICE
|
2
|
+
This is an early version and I don't expect it to work in all cases.
|
3
|
+
|
4
|
+
Currently, the biggest item that I am working on is getting dates extracted correctly.
|
5
|
+
|
6
|
+
If you have suggestions or an .adt file that has issues being read then let me know and I might be able to work on it.
|
7
|
+
|
8
|
+
# Ruby-ADT
|
9
|
+
|
10
|
+
Ruby ADT is a small fast library for reading Advantage Database Server database files (.ADT)
|
11
|
+
|
12
|
+
* Project page: <http://github.com/chasemgray/Ruby-ADT>
|
13
|
+
* Report bugs: <http://github.com/chasemgray/Ruby-ADT/issues>
|
14
|
+
* Questions: Email <mailto:chase@ratchetsoftware.com> and put ADT somewhere in the subject line
|
15
|
+
|
16
|
+
## Installation
|
17
|
+
|
18
|
+
gem install ruby-adt
|
19
|
+
|
20
|
+
## Basic Usage
|
21
|
+
|
22
|
+
Load an ADT file:
|
23
|
+
|
24
|
+
require 'rubygems'
|
25
|
+
require 'ruby-adt'
|
26
|
+
|
27
|
+
table = ADT::Table.new("test.adt")
|
28
|
+
|
29
|
+
Enumerate all records
|
30
|
+
|
31
|
+
table.each do |record|
|
32
|
+
puts record.name
|
33
|
+
puts record.email
|
34
|
+
end
|
35
|
+
|
36
|
+
Load a single record using <tt>record</tt> or <tt>find</tt>
|
37
|
+
|
38
|
+
table.record(6)
|
39
|
+
table.find(6)
|
40
|
+
|
41
|
+
Attributes can also be accessed through the attributes hash in original or
|
42
|
+
underscored form or as an accessor method using the underscored name.
|
43
|
+
|
44
|
+
table.record(4).attributes["PhoneBook"]
|
45
|
+
table.record(4).attributes["phone_book"]
|
46
|
+
table.record(4).phone_book
|
47
|
+
|
48
|
+
Search for records using a simple hash format. Multiple search criteria are
|
49
|
+
ANDed. Use the block form of find if the resulting recordset could be large
|
50
|
+
otherwise all records will be loaded into memory.
|
51
|
+
|
52
|
+
# find all records with first_name equal to Keith
|
53
|
+
table.find(:all, :first_name => 'Keith') do |record|
|
54
|
+
puts record.last_name
|
55
|
+
end
|
56
|
+
|
57
|
+
# find all records with first_name equal to Keith and last_name equal
|
58
|
+
# to Morrison
|
59
|
+
table.find(:all, :first_name => 'Keith', :last_name => 'Morrison') do |record|
|
60
|
+
puts record.last_name
|
61
|
+
end
|
62
|
+
|
63
|
+
# find the first record with first_name equal to Keith
|
64
|
+
table.find :first, :first_name => 'Keith'
|
65
|
+
|
66
|
+
# find record number 10
|
67
|
+
table.find(10)
|
68
|
+
|
69
|
+
## Migrating to ActiveRecord
|
70
|
+
|
71
|
+
An example of migrating a DBF book table to ActiveRecord using a migration:
|
72
|
+
|
73
|
+
require 'adt'
|
74
|
+
|
75
|
+
class CreateBooks < ActiveRecord::Migration
|
76
|
+
def self.up
|
77
|
+
table = ADT::Table.new('db/adt/books.adt')
|
78
|
+
eval(table.schema)
|
79
|
+
|
80
|
+
table.each do |record|
|
81
|
+
Book.create(record.attributes)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.down
|
86
|
+
drop_table :books
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
## Limitations and known bugs
|
91
|
+
|
92
|
+
* ADT is read-only
|
93
|
+
* External index files are not used
|
94
|
+
|
95
|
+
## Acknowledgements
|
96
|
+
|
97
|
+
|
98
|
+
## License
|
99
|
+
|
100
|
+
(The MIT Licence)
|
101
|
+
|
102
|
+
Copyright (c) 2010-2010 Chase Gray <mailto:chase@ratchetsoftware.com>
|
103
|
+
|
104
|
+
Permission is hereby granted, free of charge, to any person
|
105
|
+
obtaining a copy of this software and associated documentation
|
106
|
+
files (the "Software"), to deal in the Software without
|
107
|
+
restriction, including without limitation the rights to use,
|
108
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
109
|
+
copies of the Software, and to permit persons to whom the
|
110
|
+
Software is furnished to do so, subject to the following
|
111
|
+
conditions:
|
112
|
+
|
113
|
+
The above copyright notice and this permission notice shall be
|
114
|
+
included in all copies or substantial portions of the Software.
|
115
|
+
|
116
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
117
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
118
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
119
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
120
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
121
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
122
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
123
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,32 @@
|
|
1
|
+
PROJECT_ROOT = File.expand_path(File.dirname(__FILE__))
|
2
|
+
$: << File.join(PROJECT_ROOT, 'lib')
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'jeweler'
|
6
|
+
require 'spec/rake/spectask'
|
7
|
+
|
8
|
+
Jeweler::Tasks.new do |s|
|
9
|
+
s.name = 'ruby-adt'
|
10
|
+
s.description = 'A small fast library for reading Advantage Database Server database files (ADT).'
|
11
|
+
s.summary = 'Read ADT files'
|
12
|
+
s.platform = Gem::Platform::RUBY
|
13
|
+
s.authors = ['Chase Gray']
|
14
|
+
s.email = 'chase@ratchetsoftware.com'
|
15
|
+
s.add_dependency('activesupport', ['>= 2.1.0'])
|
16
|
+
s.homepage = 'http://github.com/chasemgray/Ruby-ADT'
|
17
|
+
end
|
18
|
+
|
19
|
+
Jeweler::GemcutterTasks.new
|
20
|
+
|
21
|
+
task :default => :spec
|
22
|
+
|
23
|
+
desc "Run specs"
|
24
|
+
Spec::Rake::SpecTask.new :spec do |t|
|
25
|
+
t.spec_files = FileList['spec/**/*spec.rb']
|
26
|
+
end
|
27
|
+
|
28
|
+
desc "Run spec docs"
|
29
|
+
Spec::Rake::SpecTask.new :specdoc do |t|
|
30
|
+
t.spec_opts = ["-f specdoc"]
|
31
|
+
t.spec_files = FileList['spec/**/*spec.rb']
|
32
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.5.0
|
@@ -0,0 +1,36 @@
|
|
1
|
+
04 followed by 32-bit zero (00 00 00 00) seem to precede each new row of data.
|
2
|
+
|
3
|
+
Seems like for each column we have to know how long each data set is because there is no separator between data.
|
4
|
+
|
5
|
+
The actual data is stored at the end of the file.
|
6
|
+
There is a header with unknown information followed by information about the columns.
|
7
|
+
There doesn't seem to be a footer
|
8
|
+
|
9
|
+
400 byte header
|
10
|
+
|
11
|
+
byte 24 in the header specifies the number of rows. 32-bits I believe
|
12
|
+
byte 33 in the header specifies the number of columns - 16-bits - the previous bytes also change when columns are added and removed but I don't know why. It doesn't matter for my purposes.
|
13
|
+
|
14
|
+
|
15
|
+
column names follow header
|
16
|
+
|
17
|
+
Column names have a reserved 128 characters (bytes)
|
18
|
+
column names are followed by 72 bytes of information about the column.
|
19
|
+
|
20
|
+
2 bytes after the 128 byte column name is the column type:
|
21
|
+
00 04 - character
|
22
|
+
00 0A - double
|
23
|
+
00 0B - integer
|
24
|
+
00 0C - shortint
|
25
|
+
00 14 - cicharacter
|
26
|
+
00 03 - date
|
27
|
+
00 0D - time
|
28
|
+
00 0E - timestamp
|
29
|
+
00 0F - autoinc
|
30
|
+
|
31
|
+
8th byte after 128 byte column name specifies the length in bytes
|
32
|
+
|
33
|
+
|
34
|
+
how to parse integers?
|
35
|
+
how to parse doubles?
|
36
|
+
how to parse dates and times?
|
data/lib/adt.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'date'
|
3
|
+
require 'active_support'
|
4
|
+
|
5
|
+
if RUBY_VERSION > '1.9'
|
6
|
+
require 'csv'
|
7
|
+
unless defined? FCSV
|
8
|
+
class Object
|
9
|
+
FCSV = CSV
|
10
|
+
alias_method :FCSV, :CSV
|
11
|
+
end
|
12
|
+
end
|
13
|
+
else
|
14
|
+
require 'fastercsv'
|
15
|
+
end
|
16
|
+
|
17
|
+
require 'adt/globals'
|
18
|
+
require 'adt/record'
|
19
|
+
require 'adt/column'
|
20
|
+
require 'adt/table'
|
21
|
+
|
22
|
+
|
data/lib/adt/column.rb
ADDED
@@ -0,0 +1,91 @@
|
|
1
|
+
module ADT
|
2
|
+
class ColumnLengthError < ADTError; end
|
3
|
+
class ColumnNameError < ADTError; end
|
4
|
+
|
5
|
+
TYPES = {4 => 'character', 10 => 'double', 11 => 'integer', 12 => 'short', 20 => 'cicharacter', 3 => 'date', 13 => 'time', 14 => 'timestamp', 15 => 'autoinc'}
|
6
|
+
FLAGS = {'character' => 'A', 'double' => 'D', 'integer' => 'i', 'short' => 'S', 'cicharacter' => 'A', 'date' => '?', 'time' => '?', 'timestamp' => '?', 'autoinc' => 'I'}
|
7
|
+
|
8
|
+
|
9
|
+
class Column
|
10
|
+
attr_reader :name, :type, :length
|
11
|
+
|
12
|
+
# Initialize a new ADT::Column
|
13
|
+
#
|
14
|
+
# @param [String] name
|
15
|
+
# @param [String] type
|
16
|
+
# @param [Fixnum] length
|
17
|
+
def initialize(name, type, length)
|
18
|
+
@name, @type, @length = strip_non_ascii_chars(name), type, length
|
19
|
+
|
20
|
+
raise ColumnLengthError, "field length must be greater than 0" unless length > 0
|
21
|
+
raise ColumnNameError, "column name cannot be empty" if @name.length == 0
|
22
|
+
end
|
23
|
+
|
24
|
+
def data_type(id)
|
25
|
+
TYPES[id]
|
26
|
+
end
|
27
|
+
|
28
|
+
def flag(type, length = 0)
|
29
|
+
data_type = data_type(type)
|
30
|
+
flag = FLAGS[data_type]
|
31
|
+
if flag.eql? 'A'
|
32
|
+
return flag + length.to_s
|
33
|
+
end
|
34
|
+
return flag
|
35
|
+
end
|
36
|
+
|
37
|
+
# Decode a DateTime value
|
38
|
+
#
|
39
|
+
# @param [String] value
|
40
|
+
# @return [DateTime]
|
41
|
+
def decode_datetime(value)
|
42
|
+
days, milliseconds = value.unpack('l2')
|
43
|
+
seconds = milliseconds / 1000
|
44
|
+
DateTime.jd(days, seconds/3600, seconds/60 % 60, seconds % 60) rescue nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# Schema definition
|
48
|
+
#
|
49
|
+
# @return [String]
|
50
|
+
def schema_definition
|
51
|
+
"\"#{name.underscore}\", #{schema_data_type}\n"
|
52
|
+
end
|
53
|
+
|
54
|
+
# Column type for schema definition
|
55
|
+
#
|
56
|
+
# @return [String]
|
57
|
+
def schema_data_type
|
58
|
+
case data_type(type)
|
59
|
+
when "character"
|
60
|
+
":string, :limit => #{length}"
|
61
|
+
when "cicharacter"
|
62
|
+
":string, :limit => #{length}"
|
63
|
+
when "double"
|
64
|
+
":float"
|
65
|
+
when "date"
|
66
|
+
":date"
|
67
|
+
when "time"
|
68
|
+
":timestamp"
|
69
|
+
when "timestamp"
|
70
|
+
":timestamp"
|
71
|
+
when "integer"
|
72
|
+
":integer"
|
73
|
+
when "autoinc"
|
74
|
+
":integer"
|
75
|
+
else
|
76
|
+
":string, :limit => #{length}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# Strip all non-ascii and non-printable characters
|
81
|
+
#
|
82
|
+
# @param [String] s
|
83
|
+
# @return [String]
|
84
|
+
def strip_non_ascii_chars(s)
|
85
|
+
# truncate the string at the first null character
|
86
|
+
s = s[0, s.index("\x00")] if s.index("\x00")
|
87
|
+
|
88
|
+
s.gsub(/[^\x20-\x7E]/,"")
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/adt/globals.rb
ADDED
data/lib/adt/record.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
module ADT
|
2
|
+
# An instance of ADT::Record represents a row in the ADT file
|
3
|
+
class Record
|
4
|
+
attr_reader :attributes
|
5
|
+
|
6
|
+
delegate :columns, :to => :@table
|
7
|
+
|
8
|
+
# Initialize a new ADT::Record
|
9
|
+
#
|
10
|
+
# @param [ADT::Table] table
|
11
|
+
def initialize(table)
|
12
|
+
@table, @data = table, table.data
|
13
|
+
|
14
|
+
initialize_values
|
15
|
+
define_accessors
|
16
|
+
end
|
17
|
+
|
18
|
+
# Equality
|
19
|
+
#
|
20
|
+
# @param [ADT::Record] other
|
21
|
+
# @return [Boolean]
|
22
|
+
def ==(other)
|
23
|
+
other.respond_to?(:attributes) && other.attributes == attributes
|
24
|
+
end
|
25
|
+
|
26
|
+
# Maps a row to an array of values
|
27
|
+
#
|
28
|
+
# @return [Array]
|
29
|
+
def to_a
|
30
|
+
columns.map { |column| @attributes[column.name.underscore] }
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
# Defined attribute accessor methods
|
36
|
+
def define_accessors
|
37
|
+
columns.each do |column|
|
38
|
+
underscored_column_name = column.name.underscore
|
39
|
+
unless respond_to?(underscored_column_name)
|
40
|
+
self.class.send :define_method, underscored_column_name do
|
41
|
+
@attributes[column.name.underscore]
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Initialize values for a row
|
48
|
+
def initialize_values
|
49
|
+
#skip the first 5 bytes, don't know what they are for and they don't contain the data.
|
50
|
+
@data.read(5)
|
51
|
+
|
52
|
+
@attributes = columns.inject({}) do |hash, column|
|
53
|
+
|
54
|
+
#get the unpack flag to get this data.
|
55
|
+
value = @data.read(column.length).unpack("#{column.flag(column.type, column.length)}").first
|
56
|
+
hash[column.name] = value
|
57
|
+
hash[column.name.underscore] = value
|
58
|
+
|
59
|
+
hash
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
|
64
|
+
end
|
65
|
+
end
|
data/lib/adt/table.rb
ADDED
@@ -0,0 +1,259 @@
|
|
1
|
+
module ADT
|
2
|
+
|
3
|
+
# ADT::Table is the primary interface to a single ADT file and provides
|
4
|
+
# methods for enumerating and searching the records.
|
5
|
+
class Table
|
6
|
+
attr_reader :column_count # The total number of columns
|
7
|
+
attr_reader :columns # An array of DBF::Column
|
8
|
+
attr_reader :options # The options hash used to initialize the table
|
9
|
+
attr_reader :data # ADT file handle
|
10
|
+
attr_reader :record_count # Total number of records
|
11
|
+
|
12
|
+
# Opens a ADT:Table
|
13
|
+
# Example:
|
14
|
+
# table = ADT::Table.new 'data.adt'
|
15
|
+
#
|
16
|
+
# @param [String] path Path to the adt file
|
17
|
+
def initialize(path)
|
18
|
+
@data = File.open(path, 'rb')
|
19
|
+
reload!
|
20
|
+
end
|
21
|
+
|
22
|
+
# Closes the table
|
23
|
+
def close
|
24
|
+
@data.close
|
25
|
+
end
|
26
|
+
|
27
|
+
# Reloads the database
|
28
|
+
def reload!
|
29
|
+
@records = nil
|
30
|
+
get_header_info
|
31
|
+
get_column_descriptors
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
# Retrieve a Column by name
|
36
|
+
#
|
37
|
+
# @param [String, Symbol] column_name
|
38
|
+
# @return [ADT::Column]
|
39
|
+
def column(column_name)
|
40
|
+
@columns.detect {|f| f.name == column_name.to_s}
|
41
|
+
end
|
42
|
+
|
43
|
+
# Calls block once for each record in the table. The record may be nil
|
44
|
+
# if the record has been marked as deleted.
|
45
|
+
#
|
46
|
+
# @yield [nil, ADT::Record]
|
47
|
+
def each
|
48
|
+
0.upto(@record_count - 1) do |n|
|
49
|
+
seek_to_record(n)
|
50
|
+
yield ADT::Record.new(self)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# Retrieve a record by index number
|
55
|
+
#
|
56
|
+
# @param [Fixnum] index
|
57
|
+
# @return [ADT::Record]
|
58
|
+
def record(index)
|
59
|
+
seek_to_record(index)
|
60
|
+
ADT::Record.new(self)
|
61
|
+
end
|
62
|
+
|
63
|
+
alias_method :row, :record
|
64
|
+
|
65
|
+
|
66
|
+
# Generate an ActiveRecord::Schema
|
67
|
+
#
|
68
|
+
# xBase data types are converted to generic types as follows:
|
69
|
+
# - Number columns with no decimals are converted to :integer
|
70
|
+
# - Number columns with decimals are converted to :float
|
71
|
+
# - Date columns are converted to :datetime
|
72
|
+
# - Logical columns are converted to :boolean
|
73
|
+
# - Memo columns are converted to :text
|
74
|
+
# - Character columns are converted to :string and the :limit option is set
|
75
|
+
# to the length of the character column
|
76
|
+
#
|
77
|
+
# Example:
|
78
|
+
# create_table "mydata" do |t|
|
79
|
+
# t.column :name, :string, :limit => 30
|
80
|
+
# t.column :last_update, :datetime
|
81
|
+
# t.column :is_active, :boolean
|
82
|
+
# t.column :age, :integer
|
83
|
+
# t.column :notes, :text
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# @param [optional String] path
|
87
|
+
# @return [String]
|
88
|
+
def schema(path = nil)
|
89
|
+
s = "ActiveRecord::Schema.define do\n"
|
90
|
+
s << " create_table \"#{File.basename(@data.path, ".*")}\" do |t|\n"
|
91
|
+
columns.each do |column|
|
92
|
+
s << " t.column #{column.schema_definition}"
|
93
|
+
end
|
94
|
+
s << " end\nend"
|
95
|
+
|
96
|
+
if path
|
97
|
+
File.open(path, 'w') {|f| f.puts(s)}
|
98
|
+
end
|
99
|
+
|
100
|
+
s
|
101
|
+
end
|
102
|
+
|
103
|
+
def to_a
|
104
|
+
records = []
|
105
|
+
each {|record| records << record if record}
|
106
|
+
records
|
107
|
+
end
|
108
|
+
|
109
|
+
# Dumps all records to a CSV file. If no filename is given then CSV is
|
110
|
+
# output to STDOUT.
|
111
|
+
#
|
112
|
+
# @param [optional String] path Defaults to basename of adt file
|
113
|
+
def to_csv(path = nil)
|
114
|
+
path = File.basename(@data.path, '.adt') + '.csv' if path.nil?
|
115
|
+
FCSV.open(path, 'w', :force_quotes => true) do |csv|
|
116
|
+
each do |record|
|
117
|
+
csv << record.to_a
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# Find records using a simple ActiveRecord-like syntax.
|
123
|
+
#
|
124
|
+
# Examples:
|
125
|
+
# table = ADT::Table.new 'mydata.adt'
|
126
|
+
#
|
127
|
+
# # Find record number 5
|
128
|
+
# table.find(5)
|
129
|
+
#
|
130
|
+
# # Find all records for Chase Gray
|
131
|
+
# table.find :all, :first_name => "Chase", :last_name => "Gray"
|
132
|
+
#
|
133
|
+
# # Find first record
|
134
|
+
# table.find :first, :first_name => "Chase"
|
135
|
+
#
|
136
|
+
# The <b>command</b> may be a record index, :all, or :first.
|
137
|
+
# <b>options</b> is optional and, if specified, should be a hash where the keys correspond
|
138
|
+
# to column names in the database. The values will be matched exactly with the value
|
139
|
+
# in the database. If you specify more than one key, all values must match in order
|
140
|
+
# for the record to be returned. The equivalent SQL would be "WHERE key1 = 'value1'
|
141
|
+
# AND key2 = 'value2'".
|
142
|
+
#
|
143
|
+
# @param [Fixnum, Symbol] command
|
144
|
+
# @param [optional, Hash] options Hash of search parameters
|
145
|
+
# @yield [optional, ADT::Record]
|
146
|
+
def find(command, options = {}, &block)
|
147
|
+
case command
|
148
|
+
when Fixnum
|
149
|
+
record(command)
|
150
|
+
when Array
|
151
|
+
command.map {|i| record(i)}
|
152
|
+
when :all
|
153
|
+
find_all(options, &block)
|
154
|
+
when :first
|
155
|
+
find_first(options)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
private
|
160
|
+
|
161
|
+
# Find all matching
|
162
|
+
#
|
163
|
+
# @param [Hash] options
|
164
|
+
# @yield [optional ADT::Record]
|
165
|
+
# @return [Array]
|
166
|
+
def find_all(options, &block)
|
167
|
+
results = []
|
168
|
+
each do |record|
|
169
|
+
if all_values_match?(record, options)
|
170
|
+
if block_given?
|
171
|
+
yield(record)
|
172
|
+
else
|
173
|
+
results << record
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
results
|
178
|
+
end
|
179
|
+
|
180
|
+
# Find first matching
|
181
|
+
#
|
182
|
+
# @param [Hash] options
|
183
|
+
# @return [ADT::Record, nil]
|
184
|
+
def find_first(options)
|
185
|
+
each do |record|
|
186
|
+
return record if all_values_match?(record, options)
|
187
|
+
end
|
188
|
+
nil
|
189
|
+
end
|
190
|
+
|
191
|
+
# Do all search parameters match?
|
192
|
+
#
|
193
|
+
# @param [ADT::Record] record
|
194
|
+
# @param [Hash] options
|
195
|
+
# @return [Boolean]
|
196
|
+
def all_values_match?(record, options)
|
197
|
+
options.all? {|key, value| record.attributes[key.to_s.underscore] == value}
|
198
|
+
end
|
199
|
+
|
200
|
+
|
201
|
+
# Replace the file extension
|
202
|
+
#
|
203
|
+
# @param [String] path
|
204
|
+
# @param [String] extension
|
205
|
+
# @return [String]
|
206
|
+
def replace_extname(path, extension)
|
207
|
+
path.sub(/#{File.extname(path)[1..-1]}$/, extension)
|
208
|
+
end
|
209
|
+
|
210
|
+
|
211
|
+
# Determine record count, record_count, and record length
|
212
|
+
def get_header_info
|
213
|
+
@data.rewind
|
214
|
+
|
215
|
+
#column_count_offset = 33, record_count_offset = 24, record_length_offset = 36
|
216
|
+
@record_count, @data_offset, @record_length = data.read(HEADER_LENGTH).unpack("@24 I x4 I I")
|
217
|
+
@column_count = (@data_offset-400)/200
|
218
|
+
end
|
219
|
+
|
220
|
+
|
221
|
+
# Retrieves column information from the database
|
222
|
+
def get_column_descriptors
|
223
|
+
#skip past header to get to column information
|
224
|
+
@data.seek(HEADER_LENGTH)
|
225
|
+
|
226
|
+
# column names are the first 128 bytes and column info takes up the last 72 bytes.
|
227
|
+
# byte 130 contains a 16-bit column type
|
228
|
+
# byte 136 contains a 16-bit length field
|
229
|
+
@columns = []
|
230
|
+
@column_count.times do
|
231
|
+
name, type, length = @data.read(200).unpack('A128 x S x4 S')
|
232
|
+
if length > 0
|
233
|
+
@columns << Column.new(name.strip, type, length)
|
234
|
+
end
|
235
|
+
end
|
236
|
+
# Reset the column count in case any were skipped
|
237
|
+
@column_count = @columns.size
|
238
|
+
|
239
|
+
@columns
|
240
|
+
end
|
241
|
+
|
242
|
+
|
243
|
+
# Seek to a byte offset in the record data
|
244
|
+
#
|
245
|
+
# @params [Fixnum] offset
|
246
|
+
def seek(offset)
|
247
|
+
@data.seek(@data_offset + offset)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Seek to a record
|
251
|
+
#
|
252
|
+
# @param [Fixnum] index
|
253
|
+
def seek_to_record(index)
|
254
|
+
seek(index * @record_length)
|
255
|
+
end
|
256
|
+
|
257
|
+
end
|
258
|
+
|
259
|
+
end
|
data/spec/fixtures/A.ADT
ADDED
Binary file
|
Binary file
|
@@ -0,0 +1,54 @@
|
|
1
|
+
describe DBF, :shared => true do
|
2
|
+
specify "sum of column lengths should equal record length specified in header" do
|
3
|
+
header_record_length = @table.instance_eval {@record_length}
|
4
|
+
sum_of_column_lengths = @table.columns.inject(1) {|sum, column| sum + column.length}
|
5
|
+
|
6
|
+
header_record_length.should == sum_of_column_lengths
|
7
|
+
end
|
8
|
+
|
9
|
+
specify "records should be instances of DBF::Record" do
|
10
|
+
@table.each do |record|
|
11
|
+
record.should be_an_instance_of(DBF::Record)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
specify "columns should be instances of DBF::Column" do
|
16
|
+
@table.columns.all? {|column| column.should be_an_instance_of(DBF::Column)}
|
17
|
+
end
|
18
|
+
|
19
|
+
specify "column names should not be blank" do
|
20
|
+
@table.columns.all? {|column| column.name.should_not be_empty}
|
21
|
+
end
|
22
|
+
|
23
|
+
specify "column types should be valid" do
|
24
|
+
valid_column_types = %w(C N L D M F B G P Y T I V X @ O +)
|
25
|
+
@table.columns.all? {|column| valid_column_types.should include(column.type)}
|
26
|
+
end
|
27
|
+
|
28
|
+
specify "column lengths should be instances of Fixnum" do
|
29
|
+
@table.columns.all? {|column| column.length.should be_an_instance_of(Fixnum)}
|
30
|
+
end
|
31
|
+
|
32
|
+
specify "column lengths should be larger than 0" do
|
33
|
+
@table.columns.all? {|column| column.length.should > 0}
|
34
|
+
end
|
35
|
+
|
36
|
+
specify "column decimals should be instances of Fixnum" do
|
37
|
+
@table.columns.all? {|column| column.decimal.should be_an_instance_of(Fixnum)}
|
38
|
+
end
|
39
|
+
|
40
|
+
specify "column read accessors should return the attribute after typecast" do
|
41
|
+
@table.columns do |column|
|
42
|
+
record = @table.records.first
|
43
|
+
record.send(column.name).should == record[column.name]
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
specify "column attributes should be accessible in underscored form" do
|
48
|
+
@table.columns do |column|
|
49
|
+
record = @table.records.first
|
50
|
+
record.send(column_name).should == record.send(column_name.underscore)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__) + '/../lib/')
|
2
|
+
require 'rubygems'
|
3
|
+
require 'spec'
|
4
|
+
require 'adt'
|
5
|
+
require 'fileutils'
|
6
|
+
|
7
|
+
DB_PATH = File.dirname(__FILE__) + '/fixtures' unless defined?(DB_PATH)
|
8
|
+
|
9
|
+
Spec::Runner.configure do |config|
|
10
|
+
|
11
|
+
end
|
12
|
+
|
13
|
+
self.class.send :remove_const, 'Test' if defined? Test
|
@@ -0,0 +1,137 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
2
|
+
|
3
|
+
describe DBF::Column do
|
4
|
+
|
5
|
+
context "when initialized" do
|
6
|
+
before do
|
7
|
+
@column = DBF::Column.new "ColumnName", "N", 1, 0
|
8
|
+
end
|
9
|
+
|
10
|
+
it "should set the #name accessor" do
|
11
|
+
@column.name.should == "ColumnName"
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should set the #type accessor" do
|
15
|
+
@column.type.should == "N"
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should set the #length accessor" do
|
19
|
+
@column.length.should == 1
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should set the #decimal accessor" do
|
23
|
+
@column.decimal.should == 0
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should raise an error if length is greater than 0" do
|
27
|
+
lambda { DBF::Column.new "ColumnName", "N", -1, 0 }.should raise_error(DBF::ColumnLengthError)
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should raise error on emtpy column names" do
|
31
|
+
lambda { DBF::Column.new "\xFF\xFC", "N", 1, 0 }.should raise_error(DBF::ColumnNameError)
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
context "#type_cast" do
|
37
|
+
it "should cast numbers with decimals to Float" do
|
38
|
+
value = "13.5"
|
39
|
+
column = DBF::Column.new "ColumnName", "N", 2, 1
|
40
|
+
column.type_cast(value).should == 13.5
|
41
|
+
end
|
42
|
+
|
43
|
+
it "should cast numbers with no decimals to Integer" do
|
44
|
+
value = "135"
|
45
|
+
column = DBF::Column.new "ColumnName", "N", 3, 0
|
46
|
+
column.type_cast(value).should == 135
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should cast :integer to Integer" do
|
50
|
+
value = "135"
|
51
|
+
column = DBF::Column.new "ColumnName", "I", 3, 0
|
52
|
+
column.type_cast(value).should == 135
|
53
|
+
end
|
54
|
+
|
55
|
+
it "should cast boolean to True" do
|
56
|
+
value = "y"
|
57
|
+
column = DBF::Column.new "ColumnName", "L", 1, 0
|
58
|
+
column.type_cast(value).should == true
|
59
|
+
end
|
60
|
+
|
61
|
+
it "should cast boolean to False" do
|
62
|
+
value = "n"
|
63
|
+
column = DBF::Column.new "ColumnName", "L", 1, 0
|
64
|
+
column.type_cast(value).should == false
|
65
|
+
end
|
66
|
+
|
67
|
+
it "should cast datetime columns to DateTime" do
|
68
|
+
value = "Nl%\000\300Z\252\003"
|
69
|
+
column = DBF::Column.new "ColumnName", "T", 16, 0
|
70
|
+
column.type_cast(value).should == "2002-10-10T17:04:56+00:00"
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should cast invalid datetime columns to nil" do
|
74
|
+
value = "Nl%\000\000A\000\999"
|
75
|
+
column = DBF::Column.new "ColumnName", "T", 16, 0
|
76
|
+
column.type_cast(value).should be_nil
|
77
|
+
end
|
78
|
+
|
79
|
+
it "should cast date columns to Date" do
|
80
|
+
value = "20050712"
|
81
|
+
column = DBF::Column.new "ColumnName", "D", 8, 0
|
82
|
+
column.type_cast(value).should == Date.new(2005,7,12)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
context "#schema_definition" do
|
87
|
+
it "should define an integer column if type is (N)umber with 9 decimals" do
|
88
|
+
column = DBF::Column.new "ColumnName", "N", 1, 0
|
89
|
+
column.schema_definition.should == "\"column_name\", :integer\n"
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should define a float colmn if type is (N)umber with more than 0 decimals" do
|
93
|
+
column = DBF::Column.new "ColumnName", "N", 1, 2
|
94
|
+
column.schema_definition.should == "\"column_name\", :float\n"
|
95
|
+
end
|
96
|
+
|
97
|
+
it "should define a date column if type is (D)ate" do
|
98
|
+
column = DBF::Column.new "ColumnName", "D", 8, 0
|
99
|
+
column.schema_definition.should == "\"column_name\", :date\n"
|
100
|
+
end
|
101
|
+
|
102
|
+
it "should define a datetime column if type is (D)ate" do
|
103
|
+
column = DBF::Column.new "ColumnName", "T", 16, 0
|
104
|
+
column.schema_definition.should == "\"column_name\", :datetime\n"
|
105
|
+
end
|
106
|
+
|
107
|
+
it "should define a boolean column if type is (L)ogical" do
|
108
|
+
column = DBF::Column.new "ColumnName", "L", 1, 0
|
109
|
+
column.schema_definition.should == "\"column_name\", :boolean\n"
|
110
|
+
end
|
111
|
+
|
112
|
+
it "should define a text column if type is (M)emo" do
|
113
|
+
column = DBF::Column.new "ColumnName", "M", 1, 0
|
114
|
+
column.schema_definition.should == "\"column_name\", :text\n"
|
115
|
+
end
|
116
|
+
|
117
|
+
it "should define a string column with length for any other data types" do
|
118
|
+
column = DBF::Column.new "ColumnName", "X", 20, 0
|
119
|
+
column.schema_definition.should == "\"column_name\", :string, :limit => 20\n"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "#strip_non_ascii_chars" do
|
124
|
+
before do
|
125
|
+
@column = DBF::Column.new "ColumnName", "N", 1, 0
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should strip characters below decimal 32 and above decimal 127" do
|
129
|
+
@column.strip_non_ascii_chars("--\x1F-\x68\x65\x6C\x6C\x6F world-\x80--").should == "---hello world---"
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should truncate characters with decimal 0" do
|
133
|
+
@column.strip_non_ascii_chars("--\x1F-\x68\x65\x6C\x6C\x6F \x00 world-\x80--").should == "---hello "
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
@@ -0,0 +1,124 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
2
|
+
|
3
|
+
describe DBF::Record do
|
4
|
+
|
5
|
+
def example_record(data = '')
|
6
|
+
table = mock_table(data)
|
7
|
+
DBF::Record.new(table)
|
8
|
+
end
|
9
|
+
|
10
|
+
def mock_table(data = '')
|
11
|
+
@column1 = DBF::Column.new 'ColumnName', 'N', 1, 0
|
12
|
+
|
13
|
+
returning mock('table') do |table|
|
14
|
+
table.stub!(:memo_block_size).and_return(8)
|
15
|
+
table.stub!(:memo).and_return(nil)
|
16
|
+
table.stub!(:columns).and_return([@column1])
|
17
|
+
table.stub!(:data).and_return(data)
|
18
|
+
table.stub!(:has_memo_file?).and_return(true)
|
19
|
+
table.data.stub!(:read).and_return(data)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context "when initialized" do
|
24
|
+
it "should typecast number columns no decimal places to Integer" do
|
25
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
26
|
+
table.column("ID").type.should == "N"
|
27
|
+
table.column("ID").decimal.should == 0
|
28
|
+
table.record(1).attributes['id'].should be_kind_of(Integer)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "should typecast number columns with decimals > 0 to Float" do
|
32
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
33
|
+
table.column("ID").type.should == "N"
|
34
|
+
table.column("COST").decimal.should == 2
|
35
|
+
table.record(1).attributes['cost'].should be_kind_of(Float)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should typecast memo columns to String" do
|
39
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
40
|
+
table.column("DESC").type.should == "M"
|
41
|
+
table.record(1).attributes['desc'].should be_kind_of(String)
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should typecast logical columns to True or False" do
|
45
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_30.dbf"
|
46
|
+
table.column("WEBINCLUDE").type.should == "L"
|
47
|
+
table.record(1).attributes["webinclude"].should satisfy {|v| v == true || v == false}
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe '#memo_block_content_size' do
|
52
|
+
it "should equal the difference between the table's memo_block_size and 8" do
|
53
|
+
table = mock_table
|
54
|
+
table.should_receive(:memo_block_size).and_return(128)
|
55
|
+
record = DBF::Record.new(table)
|
56
|
+
|
57
|
+
record.send(:memo_block_content_size).should == 120
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
describe '#memo_content_size' do
|
62
|
+
it "should equal 8 plus the difference between memo_size and the table's memo_block_size" do
|
63
|
+
record = example_record
|
64
|
+
record.should_receive(:memo_block_size).and_return(8)
|
65
|
+
|
66
|
+
record.send(:memo_content_size, 1024).should == 1024
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe '#read_memo' do
|
71
|
+
it 'should return nil if start_block is less than 1' do
|
72
|
+
table = mock_table
|
73
|
+
record = DBF::Record.new(table)
|
74
|
+
|
75
|
+
record.send(:read_memo, 0).should be_nil
|
76
|
+
record.send(:read_memo, -1).should be_nil
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'should return nil if memo file is missing' do
|
80
|
+
table = mock_table
|
81
|
+
table.should_receive(:has_memo_file?).and_return(false)
|
82
|
+
record = DBF::Record.new(table)
|
83
|
+
|
84
|
+
record.send(:read_memo, 5).should be_nil
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
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.record(9)
|
92
|
+
record.to_a.should == ["Ten records stored in this database", 10.0, nil, false, "0.100000000000000000", nil]
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#==' do
|
97
|
+
before do
|
98
|
+
@record = example_record
|
99
|
+
end
|
100
|
+
|
101
|
+
it 'should be false if other does not have attributes' do
|
102
|
+
other = mock('object')
|
103
|
+
(@record == other).should be_false
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'should be true if other attributes match' do
|
107
|
+
attributes = {:x => 1, :y => 2}
|
108
|
+
@record.stub!(:attributes).and_return(attributes)
|
109
|
+
other = mock('object', :attributes => attributes)
|
110
|
+
(@record == other).should be_true
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe 'unpack_data' do
|
115
|
+
before do
|
116
|
+
@record = example_record('abc')
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should unpack the data' do
|
120
|
+
@record.send(:unpack_data, 3).should == 'abc'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
@@ -0,0 +1,227 @@
|
|
1
|
+
require File.dirname(__FILE__) + "/../spec_helper"
|
2
|
+
|
3
|
+
describe DBF::Table do
|
4
|
+
context "when initialized" do
|
5
|
+
before do
|
6
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
7
|
+
end
|
8
|
+
|
9
|
+
it "should load the data file" do
|
10
|
+
@table.data.should be_kind_of(File)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "should load the memo file" do
|
14
|
+
@table.has_memo_file?.should be_true
|
15
|
+
@table.instance_eval("@memo").should be_kind_of(File)
|
16
|
+
end
|
17
|
+
|
18
|
+
it "should determine the memo file format" do
|
19
|
+
@table.memo_file_format.should == :dbt
|
20
|
+
end
|
21
|
+
|
22
|
+
it "should determine the correct memo block size" do
|
23
|
+
@table.memo_block_size.should == 512
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should determine the number of columns in each record" do
|
27
|
+
@table.columns.size.should == 15
|
28
|
+
end
|
29
|
+
|
30
|
+
it "should determine the number of records in the database" do
|
31
|
+
@table.record_count.should == 67
|
32
|
+
end
|
33
|
+
|
34
|
+
it "should determine the database version" do
|
35
|
+
@table.version.should == "83"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
context "when closed" do
|
40
|
+
before do
|
41
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
42
|
+
end
|
43
|
+
|
44
|
+
it "should close the data file" do
|
45
|
+
@table.close
|
46
|
+
lambda { @table.record(1) }.should raise_error(IOError)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe "#column" do
|
51
|
+
it "should accept a string or symbol as input" do
|
52
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
53
|
+
table.column(:IMAGE).should be_kind_of(DBF::Column)
|
54
|
+
table.column("IMAGE").should be_kind_of(DBF::Column)
|
55
|
+
end
|
56
|
+
|
57
|
+
it "should return a DBF::Field object when the column_name exists" do
|
58
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
59
|
+
table.column(:IMAGE).should be_kind_of(DBF::Column)
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should return nil when the column_name does not exist" do
|
63
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
64
|
+
table.column(:NOTANIMAGE).should be_nil
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#schema" do
|
69
|
+
it "should match the test schema fixture" do
|
70
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
71
|
+
control_schema = File.read(File.dirname(__FILE__) + '/../fixtures/dbase_83_schema.txt')
|
72
|
+
|
73
|
+
table.schema.should == control_schema
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe "#version_description" do
|
78
|
+
it "should return a text description of the database type" do
|
79
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
80
|
+
table.version_description.should == "dBase III with memo file"
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#replace_extname' do
|
85
|
+
it "should change the file extension" do
|
86
|
+
table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
87
|
+
table.send(:replace_extname, 'dbase_83.dbf', 'fpt').should == 'dbase_83.fpt'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe '#to_a' do
|
92
|
+
before do
|
93
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
94
|
+
|
95
|
+
@records = []
|
96
|
+
@table.each {|record| @records << record}
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'should return an array of records' do
|
100
|
+
@table.to_a.should == @records
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe '#to_csv' do
|
105
|
+
before do
|
106
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
107
|
+
end
|
108
|
+
|
109
|
+
after do
|
110
|
+
FileUtils.rm_f 'dbase_83.csv'
|
111
|
+
FileUtils.rm_f 'test.csv'
|
112
|
+
end
|
113
|
+
|
114
|
+
it 'should create default dump.csv' do
|
115
|
+
@table.to_csv
|
116
|
+
File.exists?('dbase_83.csv').should be_true
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'should create custom csv file' do
|
120
|
+
@table.to_csv('test.csv')
|
121
|
+
File.exists?('test.csv').should be_true
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
describe "#record" do
|
126
|
+
before do
|
127
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
128
|
+
|
129
|
+
@records = []
|
130
|
+
@table.each {|record| @records << record}
|
131
|
+
end
|
132
|
+
|
133
|
+
it "should nullify deleted records" do
|
134
|
+
@table.stub!(:deleted_record?).and_return(true)
|
135
|
+
@table.record(5).should be_nil
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
describe "#find" do
|
140
|
+
describe "with index" do
|
141
|
+
before do
|
142
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
143
|
+
end
|
144
|
+
|
145
|
+
it "should return the correct record" do
|
146
|
+
@table.find(5).should == @table.record(5)
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
describe 'with array of indexes' do
|
151
|
+
before do
|
152
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
153
|
+
end
|
154
|
+
|
155
|
+
it "should return the correct records" do
|
156
|
+
@table.find([1, 5, 10]).should == [@table.record(1), @table.record(5), @table.record(10)]
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
describe "with :all" do
|
161
|
+
before do
|
162
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
163
|
+
|
164
|
+
@records = []
|
165
|
+
@table.each {|record| @records << record}
|
166
|
+
end
|
167
|
+
|
168
|
+
it "should accept a block" do
|
169
|
+
records = []
|
170
|
+
@table.find(:all, :weight => 0.0) do |record|
|
171
|
+
records << record
|
172
|
+
end
|
173
|
+
records.should == @table.find(:all, :weight => 0.0)
|
174
|
+
end
|
175
|
+
|
176
|
+
it "should return all records if options are empty" do
|
177
|
+
@table.find(:all).should == @records
|
178
|
+
end
|
179
|
+
|
180
|
+
it "should return matching records when used with options" do
|
181
|
+
@table.find(:all, "WEIGHT" => 0.0).should == @records.select {|r| r.attributes["weight"] == 0.0}
|
182
|
+
end
|
183
|
+
|
184
|
+
it "should AND multiple search terms" do
|
185
|
+
@table.find(:all, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should == []
|
186
|
+
end
|
187
|
+
|
188
|
+
it "should match original column names" do
|
189
|
+
@table.find(:all, "WEIGHT" => 0.0).should_not be_empty
|
190
|
+
end
|
191
|
+
|
192
|
+
it "should match symbolized column names" do
|
193
|
+
@table.find(:all, :WEIGHT => 0.0).should_not be_empty
|
194
|
+
end
|
195
|
+
|
196
|
+
it "should match downcased column names" do
|
197
|
+
@table.find(:all, "weight" => 0.0).should_not be_empty
|
198
|
+
end
|
199
|
+
|
200
|
+
it "should match symbolized downcased column names" do
|
201
|
+
@table.find(:all, :weight => 0.0).should_not be_empty
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
describe "with :first" do
|
206
|
+
before do
|
207
|
+
@table = DBF::Table.new "#{DB_PATH}/dbase_83.dbf"
|
208
|
+
|
209
|
+
@records = []
|
210
|
+
@table.each {|record| @records << record}
|
211
|
+
end
|
212
|
+
|
213
|
+
it "should return the first record if options are empty" do
|
214
|
+
@table.find(:first).should == @records.first
|
215
|
+
end
|
216
|
+
|
217
|
+
it "should return the first matching record when used with options" do
|
218
|
+
@table.find(:first, "CODE" => "C").should == @records[5]
|
219
|
+
end
|
220
|
+
|
221
|
+
it "should AND multiple search terms" do
|
222
|
+
@table.find(:first, "ID" => 30, "IMAGE" => "graphics/00000001/TBC01.jpg").should be_nil
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
end
|
metadata
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-adt
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 5
|
8
|
+
- 0
|
9
|
+
version: 0.5.0
|
10
|
+
platform: ruby
|
11
|
+
authors:
|
12
|
+
- Chase Gray
|
13
|
+
autorequire:
|
14
|
+
bindir: bin
|
15
|
+
cert_chain: []
|
16
|
+
|
17
|
+
date: 2010-02-23 00:00:00 -05:00
|
18
|
+
default_executable:
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: activesupport
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 2
|
29
|
+
- 1
|
30
|
+
- 0
|
31
|
+
version: 2.1.0
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
description: A small fast library for reading Advantage Database Server database files (ADT).
|
35
|
+
email: chase@ratchetsoftware.com
|
36
|
+
executables: []
|
37
|
+
|
38
|
+
extensions: []
|
39
|
+
|
40
|
+
extra_rdoc_files:
|
41
|
+
- README.markdown
|
42
|
+
files:
|
43
|
+
- README.markdown
|
44
|
+
- Rakefile
|
45
|
+
- VERSION
|
46
|
+
- adt_format_notes.txt
|
47
|
+
- lib/adt.rb
|
48
|
+
- lib/adt/column.rb
|
49
|
+
- lib/adt/globals.rb
|
50
|
+
- lib/adt/record.rb
|
51
|
+
- lib/adt/table.rb
|
52
|
+
- spec/fixtures/A.ADT
|
53
|
+
- spec/fixtures/test.adt
|
54
|
+
- spec/functional/adt_shared.rb
|
55
|
+
- spec/spec_helper.rb
|
56
|
+
- spec/unit/column_spec.rb
|
57
|
+
- spec/unit/record_spec.rb
|
58
|
+
- spec/unit/table_spec.rb
|
59
|
+
has_rdoc: true
|
60
|
+
homepage: http://github.com/chasemgray/Ruby-ADT
|
61
|
+
licenses: []
|
62
|
+
|
63
|
+
post_install_message:
|
64
|
+
rdoc_options:
|
65
|
+
- --charset=UTF-8
|
66
|
+
require_paths:
|
67
|
+
- lib
|
68
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
69
|
+
requirements:
|
70
|
+
- - ">="
|
71
|
+
- !ruby/object:Gem::Version
|
72
|
+
segments:
|
73
|
+
- 0
|
74
|
+
version: "0"
|
75
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
76
|
+
requirements:
|
77
|
+
- - ">="
|
78
|
+
- !ruby/object:Gem::Version
|
79
|
+
segments:
|
80
|
+
- 0
|
81
|
+
version: "0"
|
82
|
+
requirements: []
|
83
|
+
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 1.3.6
|
86
|
+
signing_key:
|
87
|
+
specification_version: 3
|
88
|
+
summary: Read ADT files
|
89
|
+
test_files:
|
90
|
+
- spec/functional/adt_shared.rb
|
91
|
+
- spec/spec_helper.rb
|
92
|
+
- spec/unit/column_spec.rb
|
93
|
+
- spec/unit/record_spec.rb
|
94
|
+
- spec/unit/table_spec.rb
|