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 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
@@ -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