infused-dbf 1.0.7
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +55 -0
- data/Manifest.txt +31 -0
- data/README.txt +115 -0
- data/Rakefile +37 -0
- data/bin/dbf +40 -0
- data/dbf.gemspec +37 -0
- data/lib/dbf/column.rb +85 -0
- data/lib/dbf/globals.rb +27 -0
- data/lib/dbf/record.rb +99 -0
- data/lib/dbf/table.rb +243 -0
- data/lib/dbf.rb +7 -0
- data/spec/fixtures/dbase_03.dbf +0 -0
- data/spec/fixtures/dbase_30.dbf +0 -0
- data/spec/fixtures/dbase_30.fpt +0 -0
- data/spec/fixtures/dbase_83.dbf +0 -0
- data/spec/fixtures/dbase_83.dbt +0 -0
- data/spec/fixtures/dbase_83_schema.txt +19 -0
- data/spec/fixtures/dbase_8b.dbf +0 -0
- data/spec/fixtures/dbase_8b.dbt +0 -0
- data/spec/fixtures/dbase_f5.dbf +0 -0
- data/spec/fixtures/dbase_f5.fpt +0 -0
- data/spec/functional/dbf_shared.rb +45 -0
- data/spec/functional/format_03_spec.rb +23 -0
- data/spec/functional/format_30_spec.rb +23 -0
- data/spec/functional/format_83_spec.rb +23 -0
- data/spec/functional/format_8b_spec.rb +23 -0
- data/spec/functional/format_f5_spec.rb +23 -0
- data/spec/spec_helper.rb +10 -0
- data/spec/unit/column_spec.rb +124 -0
- data/spec/unit/record_spec.rb +99 -0
- data/spec/unit/table_spec.rb +162 -0
- metadata +103 -0
data/History.txt
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
== 1.0.7
|
2
|
+
|
3
|
+
* Remove support for original column names. All columns names are now downcased/underscored.
|
4
|
+
|
5
|
+
== 1.0.6
|
6
|
+
|
7
|
+
* DBF::Table now includes the Enumerable module
|
8
|
+
* Return nil for memo values if the memo file is missing
|
9
|
+
* Finder conditions now support the original and downcased/underscored column names
|
10
|
+
|
11
|
+
== 1.0.5
|
12
|
+
|
13
|
+
* Strip non-ascii characters from column names
|
14
|
+
|
15
|
+
== 1.0.4
|
16
|
+
|
17
|
+
* Underscore column names when dumping schemas (FieldId becomes field_id)
|
18
|
+
|
19
|
+
== 1.0.3
|
20
|
+
|
21
|
+
* Add support for Visual Foxpro Integer and Datetime columns
|
22
|
+
|
23
|
+
== 1.0.2
|
24
|
+
|
25
|
+
* Compatibility fix for Visual Foxpro memo files (ignore negative memo index values)
|
26
|
+
|
27
|
+
== 1.0.1
|
28
|
+
|
29
|
+
* Fixes error when using the command-line interface [#11984]
|
30
|
+
|
31
|
+
== 1.0.0
|
32
|
+
|
33
|
+
* Renamed classes and refactored code in preparation for adding the
|
34
|
+
ability to save records and create/compact databases.
|
35
|
+
* The Reader class has been renamed to Table
|
36
|
+
* Attributes are no longer accessed directly from the record. Use record.attribute['column_name']
|
37
|
+
instead, or use the new attribute accessors detailed under Basic Usage.
|
38
|
+
|
39
|
+
== 0.5.4
|
40
|
+
|
41
|
+
* Ignore deleted records in both memory modes
|
42
|
+
|
43
|
+
== 0.5.3
|
44
|
+
|
45
|
+
* Added a standalone dbf utility (try dbf -h for help)
|
46
|
+
|
47
|
+
== 0.5.0 / 2007-05-25
|
48
|
+
|
49
|
+
* New find method
|
50
|
+
* Full compatibility with the two flavors of memo file
|
51
|
+
* Two modes of operation:
|
52
|
+
* In memory (default): All records are loaded into memory on the first
|
53
|
+
request. Records are retrieved from memory for all subsequent requests.
|
54
|
+
* File I/O: All records are retrieved from disk on every request
|
55
|
+
* Improved documentation and more usage examples
|
data/Manifest.txt
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
History.txt
|
2
|
+
Manifest.txt
|
3
|
+
README.txt
|
4
|
+
Rakefile
|
5
|
+
bin/dbf
|
6
|
+
dbf.gemspec
|
7
|
+
lib/dbf.rb
|
8
|
+
lib/dbf/column.rb
|
9
|
+
lib/dbf/globals.rb
|
10
|
+
lib/dbf/record.rb
|
11
|
+
lib/dbf/table.rb
|
12
|
+
spec/fixtures/dbase_03.dbf
|
13
|
+
spec/fixtures/dbase_30.dbf
|
14
|
+
spec/fixtures/dbase_30.fpt
|
15
|
+
spec/fixtures/dbase_83.dbf
|
16
|
+
spec/fixtures/dbase_83.dbt
|
17
|
+
spec/fixtures/dbase_83_schema.txt
|
18
|
+
spec/fixtures/dbase_8b.dbf
|
19
|
+
spec/fixtures/dbase_8b.dbt
|
20
|
+
spec/fixtures/dbase_f5.dbf
|
21
|
+
spec/fixtures/dbase_f5.fpt
|
22
|
+
spec/functional/dbf_shared.rb
|
23
|
+
spec/functional/format_03_spec.rb
|
24
|
+
spec/functional/format_30_spec.rb
|
25
|
+
spec/functional/format_83_spec.rb
|
26
|
+
spec/functional/format_8b_spec.rb
|
27
|
+
spec/functional/format_f5_spec.rb
|
28
|
+
spec/spec_helper.rb
|
29
|
+
spec/unit/column_spec.rb
|
30
|
+
spec/unit/record_spec.rb
|
31
|
+
spec/unit/table_spec.rb
|
data/README.txt
ADDED
@@ -0,0 +1,115 @@
|
|
1
|
+
= DBF
|
2
|
+
|
3
|
+
DBF is a small fast library for reading dBase, xBase, Clipper and FoxPro database files
|
4
|
+
|
5
|
+
Copyright (c) 2006-2008 Keith Morrison <keithm@infused.org, www.infused.org>
|
6
|
+
|
7
|
+
* Official project page: http://rubyforge.org/projects/dbf
|
8
|
+
* API Documentation: http://dbf.rubyforge.org/docs
|
9
|
+
* To report bugs: http://www.rubyforge.org/tracker/?group_id=2009
|
10
|
+
* Questions: Email keithm@infused.org and put DBF somewhere in the subject
|
11
|
+
line
|
12
|
+
|
13
|
+
== Features
|
14
|
+
|
15
|
+
* No external dependencies
|
16
|
+
* Fields are type cast to the appropriate Ruby types
|
17
|
+
* Ability to dump the database schema in the portable ActiveRecord::Schema
|
18
|
+
format
|
19
|
+
|
20
|
+
== Installation
|
21
|
+
|
22
|
+
gem install dbf
|
23
|
+
|
24
|
+
== Basic Usage
|
25
|
+
|
26
|
+
require 'rubygems'
|
27
|
+
require 'dbf'
|
28
|
+
|
29
|
+
table = DBF::Table.new("widgets.dbf")
|
30
|
+
|
31
|
+
# Tables are enumerable
|
32
|
+
widget_ids = table.map { |row| row.id }
|
33
|
+
abc_names = table.select { |row| row.name =~ /^[a-cA-C] }
|
34
|
+
sorted = table.sort_by { |row| row.name }
|
35
|
+
|
36
|
+
# Print the 'name' field from record number 4
|
37
|
+
puts table.record(4).name
|
38
|
+
|
39
|
+
# Attributes can also be accessed using the column name as a Hash key
|
40
|
+
puts table.record(4).attributes["name"]
|
41
|
+
|
42
|
+
# Print the 'name' and 'address' fields from each record
|
43
|
+
table.records.each do |record|
|
44
|
+
puts record.name
|
45
|
+
puts record.email
|
46
|
+
end
|
47
|
+
|
48
|
+
# Find records
|
49
|
+
table.find :all, :first_name => 'Keith'
|
50
|
+
table.find :all, :first_name => 'Keith', :last_name => 'Morrison'
|
51
|
+
table.find :first, :first_name => 'Keith'
|
52
|
+
table.find(10)
|
53
|
+
|
54
|
+
== Migrating to ActiveRecord
|
55
|
+
|
56
|
+
An example of migrating a DBF book table to ActiveRecord using a migration:
|
57
|
+
|
58
|
+
require 'dbf'
|
59
|
+
|
60
|
+
class CreateBooks < ActiveRecord::Migration
|
61
|
+
def self.up
|
62
|
+
table = DBF::Table.new('db/dbf/books.dbf')
|
63
|
+
eval(table.schema)
|
64
|
+
|
65
|
+
table.records.each do |record|
|
66
|
+
Book.create(record.attributes)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.down
|
71
|
+
drop_table :books
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
== Command-line utility
|
76
|
+
|
77
|
+
A small command-line utility called dbf is installed along with the gem.
|
78
|
+
|
79
|
+
$ dbf -h
|
80
|
+
usage: dbf [-h|-s|-a] filename
|
81
|
+
-h = print this message
|
82
|
+
-s = print summary information
|
83
|
+
-a = create an ActiveRecord::Schema
|
84
|
+
|
85
|
+
== Limitations and known bugs
|
86
|
+
|
87
|
+
* DBF is read-only
|
88
|
+
* Index files are not used
|
89
|
+
|
90
|
+
== License
|
91
|
+
|
92
|
+
(The MIT Licence)
|
93
|
+
|
94
|
+
Copyright (c) 2006-2008 Keith Morrison <keithm@infused.org>
|
95
|
+
|
96
|
+
Permission is hereby granted, free of charge, to any person
|
97
|
+
obtaining a copy of this software and associated documentation
|
98
|
+
files (the "Software"), to deal in the Software without
|
99
|
+
restriction, including without limitation the rights to use,
|
100
|
+
copy, modify, merge, publish, distribute, sublicense, and/or sell
|
101
|
+
copies of the Software, and to permit persons to whom the
|
102
|
+
Software is furnished to do so, subject to the following
|
103
|
+
conditions:
|
104
|
+
|
105
|
+
The above copyright notice and this permission notice shall be
|
106
|
+
included in all copies or substantial portions of the Software.
|
107
|
+
|
108
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
109
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
|
110
|
+
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
111
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
|
112
|
+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
113
|
+
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
114
|
+
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
|
115
|
+
OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
require 'hoe'
|
2
|
+
require 'spec/rake/spectask'
|
3
|
+
|
4
|
+
PKG_NAME = "dbf"
|
5
|
+
PKG_VERSION = "1.0.7"
|
6
|
+
PKG_FILE_NAME = "#{PKG_NAME}-#{PKG_VERSION}"
|
7
|
+
|
8
|
+
Hoe.new PKG_NAME, PKG_VERSION do |p|
|
9
|
+
p.rubyforge_name = PKG_NAME
|
10
|
+
p.author = "Keith Morrison"
|
11
|
+
p.email = "keithm@infused.org"
|
12
|
+
p.summary = "A small fast library for reading dBase, xBase, Clipper and FoxPro database files."
|
13
|
+
p.description = p.paragraphs_of("README.txt", 1..3).join("\n\n")
|
14
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
15
|
+
p.url = "http://github.com/infused/dm-dbf/tree/master"
|
16
|
+
p.need_tar = true
|
17
|
+
p.need_zip = true
|
18
|
+
p.extra_deps << ['activesupport', '>= 2.1.0']
|
19
|
+
end
|
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
|
33
|
+
|
34
|
+
desc "Generate gemspec"
|
35
|
+
task :gemspec do |t|
|
36
|
+
`rake debug_gem > dbf.gemspec`
|
37
|
+
end
|
data/bin/dbf
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
#!/usr/bin/env ruby -ws
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'dbf'
|
5
|
+
|
6
|
+
$a ||= false
|
7
|
+
$s ||= false
|
8
|
+
|
9
|
+
if defined? $h then
|
10
|
+
puts "usage: #{File.basename(__FILE__)} [-h|-s|-a] filename"
|
11
|
+
puts " -h = print this message"
|
12
|
+
puts " -s = print summary information"
|
13
|
+
puts " -a = create an ActiveRecord::Schema"
|
14
|
+
else
|
15
|
+
|
16
|
+
filename = ARGV.shift
|
17
|
+
abort "You must supply a filename on the command line" unless filename
|
18
|
+
|
19
|
+
# create an ActiveRecord::Schema
|
20
|
+
if $a
|
21
|
+
table = DBF::Table.new filename
|
22
|
+
puts table.schema
|
23
|
+
end
|
24
|
+
|
25
|
+
if $s
|
26
|
+
table = DBF::Table.new filename
|
27
|
+
puts
|
28
|
+
puts "Database: #{filename}"
|
29
|
+
puts "Type: (#{table.version}) #{table.version_description}"
|
30
|
+
puts "Memo Type: #{table.memo_file_format}" if table.has_memo_file?
|
31
|
+
puts "Records: #{table.record_count}"
|
32
|
+
|
33
|
+
puts "\nFields:"
|
34
|
+
puts "Name Type Length Decimal"
|
35
|
+
puts "-" * 78
|
36
|
+
table.columns.each do |f|
|
37
|
+
puts "%-16s %-10s %-10s %-10s" % [f.name, f.type, f.length, f.decimal]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
data/dbf.gemspec
ADDED
@@ -0,0 +1,37 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = %q{dbf}
|
3
|
+
s.version = "1.0.7"
|
4
|
+
|
5
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
6
|
+
s.authors = ["Keith Morrison"]
|
7
|
+
s.date = %q{2009-01-01}
|
8
|
+
s.default_executable = %q{dbf}
|
9
|
+
s.description = %q{DBF is a small fast library for reading dBase, xBase, Clipper and FoxPro database files Copyright (c) 2006-2008 Keith Morrison <keithm@infused.org, www.infused.org> * Official project page: http://rubyforge.org/projects/dbf * API Documentation: http://dbf.rubyforge.org/docs * To report bugs: http://www.rubyforge.org/tracker/?group_id=2009 * Questions: Email keithm@infused.org and put DBF somewhere in the subject line}
|
10
|
+
s.email = %q{keithm@infused.org}
|
11
|
+
s.executables = ["dbf"]
|
12
|
+
s.extra_rdoc_files = ["History.txt", "Manifest.txt", "README.txt"]
|
13
|
+
s.files = ["History.txt", "Manifest.txt", "README.txt", "Rakefile", "bin/dbf", "dbf.gemspec", "lib/dbf.rb", "lib/dbf/column.rb", "lib/dbf/globals.rb", "lib/dbf/record.rb", "lib/dbf/table.rb", "spec/fixtures/dbase_03.dbf", "spec/fixtures/dbase_30.dbf", "spec/fixtures/dbase_30.fpt", "spec/fixtures/dbase_83.dbf", "spec/fixtures/dbase_83.dbt", "spec/fixtures/dbase_83_schema.txt", "spec/fixtures/dbase_8b.dbf", "spec/fixtures/dbase_8b.dbt", "spec/fixtures/dbase_f5.dbf", "spec/fixtures/dbase_f5.fpt", "spec/functional/dbf_shared.rb", "spec/functional/format_03_spec.rb", "spec/functional/format_30_spec.rb", "spec/functional/format_83_spec.rb", "spec/functional/format_8b_spec.rb", "spec/functional/format_f5_spec.rb", "spec/spec_helper.rb", "spec/unit/column_spec.rb", "spec/unit/record_spec.rb", "spec/unit/table_spec.rb"]
|
14
|
+
s.has_rdoc = true
|
15
|
+
s.homepage = %q{http://github.com/infused/dm-dbf/tree/master}
|
16
|
+
s.rdoc_options = ["--main", "README.txt"]
|
17
|
+
s.require_paths = ["lib"]
|
18
|
+
s.rubyforge_project = %q{dbf}
|
19
|
+
s.rubygems_version = %q{1.3.1}
|
20
|
+
s.summary = %q{A small fast library for reading dBase, xBase, Clipper and FoxPro database files.}
|
21
|
+
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
24
|
+
s.specification_version = 2
|
25
|
+
|
26
|
+
if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
|
27
|
+
s.add_runtime_dependency(%q<activesupport>, [">= 2.1.0"])
|
28
|
+
s.add_development_dependency(%q<hoe>, [">= 1.8.2"])
|
29
|
+
else
|
30
|
+
s.add_dependency(%q<activesupport>, [">= 2.1.0"])
|
31
|
+
s.add_dependency(%q<hoe>, [">= 1.8.2"])
|
32
|
+
end
|
33
|
+
else
|
34
|
+
s.add_dependency(%q<activesupport>, [">= 2.1.0"])
|
35
|
+
s.add_dependency(%q<hoe>, [">= 1.8.2"])
|
36
|
+
end
|
37
|
+
end
|
data/lib/dbf/column.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
module DBF
|
2
|
+
class ColumnLengthError < DBFError; end
|
3
|
+
class ColumnNameError < DBFError; end
|
4
|
+
|
5
|
+
class Column
|
6
|
+
attr_reader :name, :type, :length, :decimal
|
7
|
+
|
8
|
+
def initialize(name, type, length, decimal)
|
9
|
+
raise ColumnLengthError, "field length must be greater than 0" unless length > 0
|
10
|
+
@name, @type, @length, @decimal = strip_non_ascii_chars(name), type, length, decimal
|
11
|
+
end
|
12
|
+
|
13
|
+
def type_cast(value)
|
14
|
+
value = value.is_a?(Array) ? value.first : value
|
15
|
+
|
16
|
+
case type
|
17
|
+
when 'N' # number
|
18
|
+
decimal.zero? ? unpack_integer(value) : value.to_f
|
19
|
+
when 'D' # date
|
20
|
+
value.to_date unless value.blank?
|
21
|
+
when 'L' # logical
|
22
|
+
value.strip =~ /^(y|t)$/i ? true : false
|
23
|
+
when 'I' # integer
|
24
|
+
unpack_integer(value)
|
25
|
+
when 'T' # datetime
|
26
|
+
decode_datetime(value)
|
27
|
+
else
|
28
|
+
value.to_s.strip
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def decode_datetime(value)
|
33
|
+
days, milliseconds = value.unpack('l2')
|
34
|
+
hours = (milliseconds / MS_PER_HOUR).to_i
|
35
|
+
minutes = ((milliseconds - (hours * MS_PER_HOUR)) / MS_PER_MINUTE).to_i
|
36
|
+
seconds = ((milliseconds - (hours * MS_PER_HOUR) - (minutes * MS_PER_MINUTE)) / MS_PER_SECOND).to_i
|
37
|
+
DateTime.jd(days, hours, minutes, seconds)
|
38
|
+
end
|
39
|
+
|
40
|
+
def unpack_integer(value)
|
41
|
+
value.unpack('v').first.to_i
|
42
|
+
end
|
43
|
+
|
44
|
+
def schema_definition
|
45
|
+
data_type = case type
|
46
|
+
when "N" # number
|
47
|
+
if decimal > 0
|
48
|
+
":float"
|
49
|
+
else
|
50
|
+
":integer"
|
51
|
+
end
|
52
|
+
when "I" # integer
|
53
|
+
":integer"
|
54
|
+
when "D" # date
|
55
|
+
":date"
|
56
|
+
when "T" # datetime
|
57
|
+
":datetime"
|
58
|
+
when "L" # boolean
|
59
|
+
":boolean"
|
60
|
+
when "M" # memo
|
61
|
+
":text"
|
62
|
+
else
|
63
|
+
":string, :limit => #{length}"
|
64
|
+
end
|
65
|
+
|
66
|
+
"\"#{name.underscore}\", #{data_type}\n"
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def strip_non_ascii_chars(s)
|
72
|
+
clean = ''
|
73
|
+
s.each_byte do |char|
|
74
|
+
if char > 31 && char < 127
|
75
|
+
clean << char
|
76
|
+
else
|
77
|
+
raise ColumnNameError 'column name must not be empty' if clean.length == 0
|
78
|
+
return clean if char == 0
|
79
|
+
end
|
80
|
+
end
|
81
|
+
clean
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
data/lib/dbf/globals.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module DBF
|
2
|
+
DBF_HEADER_SIZE = 32
|
3
|
+
FPT_HEADER_SIZE = 512
|
4
|
+
BLOCK_HEADER_SIZE = 8
|
5
|
+
DATE_REGEXP = /([\d]{4})([\d]{2})([\d]{2})/
|
6
|
+
VERSION_DESCRIPTIONS = {
|
7
|
+
"02" => "FoxBase",
|
8
|
+
"03" => "dBase III without memo file",
|
9
|
+
"04" => "dBase IV without memo file",
|
10
|
+
"05" => "dBase V without memo file",
|
11
|
+
"30" => "Visual FoxPro",
|
12
|
+
"31" => "Visual FoxPro with AutoIncrement field",
|
13
|
+
"7b" => "dBase IV with memo file",
|
14
|
+
"83" => "dBase III with memo file",
|
15
|
+
"8b" => "dBase IV with memo file",
|
16
|
+
"8e" => "dBase IV with SQL table",
|
17
|
+
"f5" => "FoxPro with memo file",
|
18
|
+
"fb" => "FoxPro without memo file"
|
19
|
+
}
|
20
|
+
|
21
|
+
MS_PER_SECOND = 1000
|
22
|
+
MS_PER_MINUTE = MS_PER_SECOND * 60
|
23
|
+
MS_PER_HOUR = MS_PER_MINUTE * 60
|
24
|
+
|
25
|
+
class DBFError < StandardError; end
|
26
|
+
|
27
|
+
end
|
data/lib/dbf/record.rb
ADDED
@@ -0,0 +1,99 @@
|
|
1
|
+
module DBF
|
2
|
+
class Record
|
3
|
+
attr_reader :attributes
|
4
|
+
|
5
|
+
def initialize(table)
|
6
|
+
@table, @data, @memo = table, table.data, table.memo
|
7
|
+
initialize_values(table.columns)
|
8
|
+
define_accessors
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(other)
|
12
|
+
other.respond_to?(:attributes) && other.attributes == attributes
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def define_accessors
|
18
|
+
@table.columns.each do |column|
|
19
|
+
underscored_column_name = column.name.underscore
|
20
|
+
unless respond_to?(underscored_column_name)
|
21
|
+
self.class.send :define_method, underscored_column_name do
|
22
|
+
@attributes[column.name.underscore]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def initialize_values(columns)
|
29
|
+
@attributes = columns.inject({}) do |hash, column|
|
30
|
+
if column.type == 'M'
|
31
|
+
starting_block = unpack_string(column).to_i
|
32
|
+
hash[column.name.underscore] = read_memo(starting_block)
|
33
|
+
else
|
34
|
+
value = unpack_column(column)
|
35
|
+
hash[column.name.underscore] = column.type_cast(value)
|
36
|
+
end
|
37
|
+
hash
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def unpack_column(column)
|
42
|
+
@data.read(column.length).to_s.unpack("a#{column.length}")
|
43
|
+
end
|
44
|
+
|
45
|
+
def unpack_string(column)
|
46
|
+
unpack_column(column).to_s
|
47
|
+
end
|
48
|
+
|
49
|
+
def read_memo(start_block)
|
50
|
+
return nil if !@table.has_memo_file? || start_block < 1
|
51
|
+
|
52
|
+
@table.memo_file_format == :fpt ? build_fpt_memo(start_block) : build_dbt_memo(start_block)
|
53
|
+
end
|
54
|
+
|
55
|
+
def build_fpt_memo(start_block)
|
56
|
+
@memo.seek(start_block * memo_block_size)
|
57
|
+
|
58
|
+
memo_type, memo_size, memo_string = @memo.read(memo_block_size).unpack("NNa56")
|
59
|
+
return nil unless memo_type == 1 and memo_size > 0
|
60
|
+
|
61
|
+
if memo_size > memo_block_content_size
|
62
|
+
memo_string << @memo.read(memo_content_size(memo_size))
|
63
|
+
else
|
64
|
+
memo_string = memo_string[0, memo_size]
|
65
|
+
end
|
66
|
+
memo_string
|
67
|
+
end
|
68
|
+
|
69
|
+
def build_dbt_memo(start_block)
|
70
|
+
@memo.seek(start_block * memo_block_size)
|
71
|
+
|
72
|
+
case @table.version
|
73
|
+
when "83" # dbase iii
|
74
|
+
memo_string = ""
|
75
|
+
loop do
|
76
|
+
memo_string << block = @memo.read(memo_block_size)
|
77
|
+
break if block.rstrip.size < memo_block_size
|
78
|
+
end
|
79
|
+
when "8b" # dbase iv
|
80
|
+
memo_type, memo_size = @memo.read(BLOCK_HEADER_SIZE).unpack("LL")
|
81
|
+
memo_string = @memo.read(memo_size)
|
82
|
+
end
|
83
|
+
memo_string
|
84
|
+
end
|
85
|
+
|
86
|
+
def memo_block_size
|
87
|
+
@memo_block_size ||= @table.memo_block_size
|
88
|
+
end
|
89
|
+
|
90
|
+
def memo_block_content_size
|
91
|
+
memo_block_size - BLOCK_HEADER_SIZE
|
92
|
+
end
|
93
|
+
|
94
|
+
def memo_content_size(memo_size)
|
95
|
+
(memo_size - memo_block_size) + BLOCK_HEADER_SIZE
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
end
|