infused-dbf 1.0.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/History.txt +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
|