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.
@@ -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.
@@ -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?
@@ -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
+
@@ -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
@@ -0,0 +1,13 @@
1
+ module ADT
2
+ class ADTError < StandardError; end
3
+
4
+
5
+
6
+ MS_PER_SECOND = 1000
7
+ MS_PER_MINUTE = MS_PER_SECOND * 60
8
+ MS_PER_HOUR = MS_PER_MINUTE * 60
9
+
10
+ HEADER_LENGTH = 400
11
+
12
+ COLUMN_LENGTH = 200
13
+ end
@@ -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
@@ -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
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
@@ -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