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