rbase 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,26 @@
1
+ require 'rake'
2
+ require 'rubygems'
3
+
4
+ task :default => ['gem']
5
+
6
+ desc 'Create a gem'
7
+ task :gem do
8
+ spec = Gem::Specification.new do |s|
9
+ s.name = 'rbase'
10
+ s.version = '0.1'
11
+ s.summary = 'Library to create/read/write to XBase databases (*.DBF files)'
12
+ s.files = Dir.glob('**/*').delete_if { |item| item.include?('.svn') }
13
+ s.require_path = 'lib'
14
+ s.autorequire = 'rbase'
15
+ s.authors = 'Maxim Kulkin, Leonardo Augusto Pires'
16
+ s.email = 'maxim.kulkin@gmail.com, leonardo.pires@gmail.com'
17
+ s.homepage = 'http://rbase.rubyforge.com/'
18
+ s.rubyforge_project = 'rbase'
19
+ s.has_rdoc = true
20
+
21
+ s.required_ruby_version = '>= 1.8.2'
22
+ end
23
+
24
+ Gem.manage_gems
25
+ Gem::Builder.new(spec).build
26
+ end
@@ -0,0 +1,6 @@
1
+ require 'rbase/columns'
2
+ require 'rbase/schema'
3
+ require 'rbase/table'
4
+ require 'rbase/record'
5
+ require 'rbase/schema_dumper'
6
+ require 'rbase/builder'
@@ -0,0 +1,31 @@
1
+ require 'rbase/schema'
2
+
3
+ module RBase
4
+ LANGUAGE_RUSSIAN_DOS = 0x66
5
+ LANGUAGE_RUSSIAN_WINDOWS = 0xc9
6
+
7
+ # Create new XBase table file. Table file name will be equal to name with ".dbf" suffix.
8
+ #
9
+ # For list of available options see Table::create documentation.
10
+ #
11
+ # == Example
12
+ #
13
+ # XBase.create_table 'people' do |t|
14
+ # t.column :name, :string, :size => 30
15
+ # t.column :birthdate, :date
16
+ # t.column :active, :boolean
17
+ # t.column :tax, :integer, :size => 10, :decimal => 2
18
+ # end
19
+ #
20
+ # For documentation on column parameters see XBase::Schema.column documentation.
21
+ #
22
+ def self.create_table(name, options = {})
23
+ options[:language] ||= LANGUAGE_RUSSIAN_WINDOWS
24
+
25
+ schema = Schema.new
26
+ yield schema if block_given?
27
+
28
+ Table.create name, schema, options
29
+ end
30
+
31
+ end
@@ -0,0 +1,192 @@
1
+ module RBase
2
+ module Columns
3
+
4
+ # Base class for all column types
5
+ class Column
6
+ @@types = {}
7
+
8
+ # Assigns column type string to current class
9
+ def self.column_type(type)
10
+ @type = type
11
+ @@types[type] = self
12
+ end
13
+
14
+ # Returns column type class that correspond to given column type string
15
+ def self.column_for(type)
16
+ throw "Unknown column type '#{type}'" unless @@types.has_key?(type)
17
+ @@types[type]
18
+ end
19
+
20
+ # Returns column type as 1 character string
21
+ def self.type
22
+ @type
23
+ end
24
+
25
+ # Returns column type as 1 character string
26
+ def type
27
+ self.class.type
28
+ end
29
+
30
+ # Column name
31
+ attr_reader :name
32
+
33
+ # Column offset from the beginning of the record
34
+ attr_reader :offset
35
+
36
+ # Column size in characters
37
+ attr_reader :size
38
+
39
+ # Number of decimal places
40
+ attr_reader :decimal
41
+
42
+ def initialize(name, options = {})
43
+ options.merge({:name => name, :type => self.class.type}).each { |k, v| self.instance_variable_set("@#{k}", v) }
44
+ end
45
+
46
+ # Packs column value for storing it in XBase file.
47
+ def pack(value)
48
+ throw "Not implemented"
49
+ end
50
+
51
+ # Unpacks stored in XBase column data into appropriate Ruby form.
52
+ def unpack(value)
53
+ throw "Not implemented"
54
+ end
55
+ end
56
+
57
+
58
+ class CharacterColumn < Column
59
+ column_type 'C'
60
+
61
+ def initialize(name, options = {})
62
+ if options[:size] && options[:decimal]
63
+ size = options[:decimal]*256 + options[:size]
64
+ else
65
+ size = options[:size] || 254
66
+ end
67
+
68
+ super name, options.merge(:size => size)
69
+ end
70
+
71
+ def pack(value)
72
+ [value].pack("A#{size}")
73
+ end
74
+
75
+ def unpack(data)
76
+ data.rstrip
77
+ end
78
+ end
79
+
80
+
81
+ class NumberColumn < Column
82
+ column_type 'N'
83
+
84
+ def initialize(name, options = {})
85
+ size = options[:size] || 18
86
+ size = 18 if size > 18
87
+
88
+ super name, options.merge(:size => size)
89
+ end
90
+
91
+ def pack(value)
92
+ if decimal && decimal!=0
93
+ [format("%-#{size-decimal-1}.#{decimal}f", value)].pack("A#{size}")
94
+ else
95
+ [format("%-#{size}d", value)].pack("A#{size}")
96
+ end
97
+ end
98
+
99
+ def unpack(data)
100
+ return nil if data.rstrip == ''
101
+ data.rstrip.to_i
102
+ end
103
+ end
104
+
105
+
106
+ class LogicalColumn < Column
107
+ column_type 'L'
108
+
109
+ def initialize(name, options = {})
110
+ super name, options.merge(:size => 1)
111
+ end
112
+
113
+ def pack(value)
114
+ case value
115
+ when true then 'T'
116
+ when false then 'F'
117
+ else '?'
118
+ end
119
+ end
120
+
121
+ def unpack(data)
122
+ case data.upcase
123
+ when 'Y', 'T'
124
+ true
125
+ when 'N', 'F'
126
+ false
127
+ else
128
+ nil
129
+ end
130
+ end
131
+ end
132
+
133
+
134
+ class DateColumn < Column
135
+ column_type 'D'
136
+
137
+ def initialize(name, options = {})
138
+ super name, options.merge(:size => 8)
139
+ end
140
+
141
+ def pack(value)
142
+ value ? value.strftime('%Y%m%d'): ' '*8
143
+ end
144
+
145
+ def unpack(data)
146
+ return nil if data.rstrip == ''
147
+ Date.new(*data.unpack("a4a2a2").map { |s| s.to_i})
148
+ end
149
+ end
150
+
151
+
152
+ class MemoColumn < Column
153
+ column_type 'M'
154
+
155
+ def initialize(name, options = {})
156
+ super name, options.merge(:size => 10)
157
+ end
158
+
159
+ def pack(value)
160
+ # Not implemented yet.. using stub
161
+ [].pack('A10')
162
+ end
163
+
164
+ def unpack(data)
165
+ # Not implemented yet.. using stub
166
+ ''
167
+ end
168
+ end
169
+
170
+
171
+ class FloatColumn < Column
172
+ column_type 'F'
173
+
174
+ def initialize(name, options = {})
175
+ super name, options.merge(:size => 20)
176
+ end
177
+
178
+ def decimal
179
+ (@decimal && @decimal <= 15) ? @decimal : 2
180
+ end
181
+
182
+ def pack(value)
183
+ [format("%-#{size-decimal-1}.#{decimal}f", value)].pack("A#{size}")
184
+ end
185
+
186
+ def unpack(data)
187
+ data.rstrip.to_f
188
+ end
189
+ end
190
+
191
+ end
192
+ end
@@ -0,0 +1,107 @@
1
+ module RBase
2
+
3
+ # Class that contains data for particular table row.
4
+ # Should not be created explicitly (use Table#create to create records)
5
+ #
6
+ # == Accessing attributes
7
+ #
8
+ # You can read and assign values to row's columns using simple property syntax:
9
+ #
10
+ # user = users_table[0]
11
+ # user.name = 'Bob'
12
+ # user.birth_date = Date.new(1980, 2, 29)
13
+ # user.save
14
+ #
15
+ # puts user.name
16
+ # puts user.birth_date
17
+ #
18
+ class Record
19
+ attr_reader :table, :index
20
+
21
+ def initialize(table)
22
+ @table = table
23
+ @values_cached = {}
24
+ @values_changed = {}
25
+ end
26
+
27
+ private
28
+
29
+ def load(index, data)
30
+ @table = table
31
+ @index = index
32
+ @data = data.dup
33
+ end
34
+
35
+ def create(attributes = {})
36
+ attributes.each { |k, v| @values_changed[k.to_s.upcase] = v }
37
+ end
38
+
39
+ public
40
+
41
+ # Returns true if record was never saved to database; otherwise return false.
42
+ def new_record?
43
+ @data.nil?
44
+ end
45
+
46
+ # Save record to database.
47
+ def save
48
+ record = self
49
+ @table.instance_eval { save(record) }
50
+ end
51
+
52
+ # Delete record from database.
53
+ def delete
54
+ @deleted = true
55
+ save
56
+ end
57
+
58
+ # Returns true if record was marked as deleted; otherwise return false.
59
+ def deleted?
60
+ @deleted ||= new_record? ? false : @data[0, 1] == '*'
61
+ end
62
+
63
+ def method_missing(sym, *args)
64
+ name = sym.to_s
65
+ if /=$/ =~ name && args.size == 1
66
+ set_value(name[0..-2], args.first)
67
+ else
68
+ get_value(name)
69
+ end
70
+ end
71
+
72
+ def serialize
73
+ if new_record?
74
+ @data = deleted? ? '*' : ' '
75
+ @data << @table.columns.collect do |column|
76
+ column.pack(@values_changed[column.name])
77
+ end.join
78
+ else
79
+ @data[0, 1] = deleted? ? '*' : ' '
80
+ values_changed.each do |k, v|
81
+ column = @table.column(name)
82
+ @data[column.offset, column.size] = column.pack(value)
83
+ values_cached[k] = v
84
+ end
85
+ end
86
+ @data
87
+ end
88
+
89
+ protected
90
+
91
+ # Returns value of specified column
92
+ def get_value(name)
93
+ name = name.to_s.upcase.to_sym
94
+ return @values_changed[name] if @values_changed.has_key?(name)
95
+ return nil if new_record?
96
+ column = @table.column(name)
97
+ @values_cached[name] ||= column.unpack(@data[column.offset, column.size])
98
+ end
99
+
100
+ # Sets value of specified column.
101
+ def set_value(name, value)
102
+ name = name.to_s.upcase.to_sym
103
+ @values_changed[name] = value
104
+ end
105
+ end
106
+
107
+ end
@@ -0,0 +1,52 @@
1
+ require 'rbase/columns'
2
+
3
+ module RBase
4
+
5
+ class Schema
6
+ # Returns list of all columns defined.
7
+ attr_reader :columns
8
+
9
+ def initialize
10
+ @columns = []
11
+ end
12
+
13
+ # Declares new column.
14
+ #
15
+ # Options:
16
+ #
17
+ # * :size - size of the column in characters
18
+ # * :decimal - number of decimal positions
19
+ #
20
+ # There are column types that require it's size to be specified (but they still have reasonable defaults).
21
+ # But some column types (e.g. :date type) have fixes size that cannot be overriden.
22
+ #
23
+ # There are several column types available:
24
+ #
25
+ # * :string - corresponds to fixed length character column. Column size is limited to 254 (default).
26
+ # * :date - date column type
27
+ # * :boolean - logical column type
28
+ # * :integer - number column type. Number is stored in human readable form
29
+ # (text representation), so you should specify it's size in characters. Maximum column size is 18 (default).
30
+ # If :decimal option not equal to 0, number contains <:decimal> fraction positions.
31
+ # You should adjust :size keeping :decimal positions + 1 (for decimal point) in mind.
32
+ # * :memo - memo column. Memo is a text field that can be more than 254 chars long. Memo data is stored in separate file.
33
+ # This column type is not yet supported.
34
+ #
35
+ #
36
+ def column(name, type, options = {})
37
+ name = name.to_s.upcase
38
+ case type
39
+ when :string then type = 'C'
40
+ when :integer then type = 'N'
41
+ when :float then
42
+ type = 'N'
43
+ options[:decimal] ||= 6
44
+ when :boolean then type = 'L'
45
+ when :date then type = 'D'
46
+ end
47
+
48
+ @columns << Columns::Column.column_for(type).new(name, options)
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,29 @@
1
+ module RBase
2
+
3
+ class SchemaDumper
4
+ # Produce ruby schema for a given table.
5
+ #
6
+ # Parameters
7
+ # table - instance of XBase::Table opened
8
+ #
9
+ # == Example
10
+ #
11
+ # users = XBase::Table.open('users')
12
+ # File.open('users.dump.rb', 'w') do |f|
13
+ # f.write XBase::SchemaDumper.dump(users)
14
+ # end
15
+ # users.close
16
+ #
17
+ def self.dump(table)
18
+ output = ''
19
+ output << "XBase.create_table :#{table.name} do |t|\n"
20
+
21
+ table.columns.each do |column|
22
+ output << " t.column '#{column.name}', '#{column.type}', :size => #{column.size}#{ (column.decimal && column.decimal > 0) ? ", :decimal => #{column.decimal}" : ''}\n"
23
+ end
24
+
25
+ output << "end\n"
26
+ end
27
+ end
28
+
29
+ end
@@ -0,0 +1,204 @@
1
+ module RBase
2
+
3
+ class Table
4
+ private_class_method :new
5
+
6
+ # Create new XBase table file. Table file name will be equal to name with ".dbf" suffix.
7
+ #
8
+ # Allowed options
9
+ # * :language - language character set used in database. Can be one of LANGUAGE_* constants
10
+ def self.create(name, schema, options = {})
11
+ date = Date.today
12
+
13
+ record_size = 1+schema.columns.inject(0) { |size, column| size + column.size }
14
+
15
+ data = ''
16
+ data << [0xf5].pack('C') # version
17
+ #data << [0x3].pack('C') # version
18
+ data << [date.year % 100, date.month, date.day].pack('CCC') # last modification date
19
+ data << [0].pack('L') # number of records
20
+ # data << [32+schema.columns.size*32+263+1].pack('v') # data size
21
+ data << [32+schema.columns.size*32+1].pack('v') # data size
22
+ data << [record_size].pack('v') # record size
23
+ data << [].pack('x2') # reserved
24
+ data << [].pack('x') # incomplete transaction
25
+ data << [].pack('x') # encyption flag
26
+ data << [].pack('x4') # reserved
27
+ data << [].pack('x8') # reserved
28
+ data << [0].pack('c') # mdx flag
29
+ data << [options[:language]].pack('C') # language driver
30
+ data << [].pack('x2') # reserved
31
+
32
+ offset = 1 # take into account 1 byte for deleted flag
33
+ data << schema.columns.collect do |column|
34
+ s = ''
35
+ s << [column.name.to_s[0..9]].pack('a11') # field name
36
+ s << [column.type].pack('a') # field type
37
+ s << [offset].pack('L') # field data offset
38
+ s << [column.size].pack('C') # field size
39
+ s << [column.decimal || 0].pack('C') # decimal count
40
+ s << [].pack('x2') # reserved
41
+ s << [].pack('x') # work area id
42
+ s << [].pack('x2') # reserved
43
+ s << [].pack('x') # flag for SET FIELDS
44
+ s << [].pack('x7') # reserved
45
+ s << [].pack('x') # index field flag
46
+ offset += column.size
47
+ s
48
+ end.join
49
+
50
+ data << [13].pack('C') # terminator
51
+
52
+ data << [26].pack('C') # end of file
53
+
54
+ File.open("#{name}.dbf", 'wb') do |f|
55
+ f.write data
56
+ end
57
+ end
58
+
59
+ # Open table with given name.
60
+ # Table name should be like file name without ".dbf" suffix.
61
+ def self.open(name)
62
+ table = new
63
+ table.instance_eval { open("#{name}.dbf") }
64
+ table
65
+ end
66
+
67
+ # Physically remove records that were marked as deleted from file.
68
+ def pack
69
+ packed_count = 0
70
+ count.times do |i|
71
+ @file.pos = @record_offset + @record_size*i
72
+ data = @file.read(@record_size)
73
+ unless data[0, 1]=='*'
74
+ if i!=packed_count
75
+ @file.pos = @record_offset + @record_size*packed_count
76
+ @file.write data
77
+ end
78
+ packed_count += 1
79
+ end
80
+ end
81
+
82
+ file_end = @record_offset + @record_size*packed_count
83
+ @file.pos = file_end
84
+ @file.write "\x1a"
85
+ @file.truncate file_end+1
86
+
87
+ self.count = packed_count
88
+ update_header
89
+ end
90
+
91
+ def close
92
+ @file.close
93
+ end
94
+
95
+ attr_reader :name, :count, :columns, :last_modified_on, :language
96
+
97
+ # Return instance of XBase::Column for given column name
98
+ def column(name)
99
+ @name_to_columns[name]
100
+ end
101
+
102
+ # Create new record and populate it with given attributes
103
+ def create(attributes = {})
104
+ record = Record.new(self)
105
+ record.instance_eval { create(attributes) }
106
+ record
107
+ end
108
+
109
+ # Load record stored in position 'index'
110
+ def load(index)
111
+ @file.pos = @record_offset + @record_size*index
112
+ data = @file.read(@record_size)
113
+ record = Record.new(self)
114
+ record.instance_eval { load(index, data) }
115
+ record
116
+ end
117
+
118
+ alias_method :[], :load
119
+
120
+ def []=(index, record)
121
+ record.instance_eval { @index = index }
122
+ save(record)
123
+ end
124
+
125
+ # Iterate through all (even deleted) records
126
+ def each_with_deleted
127
+ return unless block_given?
128
+
129
+ count.times do |i|
130
+ yield load(i)
131
+ end
132
+ end
133
+
134
+ # Iterate through all non-deleted records
135
+ def each
136
+ return unless block_given?
137
+
138
+ self.each_with_deleted do |record|
139
+ yield record unless record.deleted?
140
+ end
141
+ end
142
+
143
+ private
144
+
145
+ def open(name)
146
+ @name = File.basename(name, '.dbf')
147
+ @file = File.open(name, "r+b")
148
+ header = @file.read(32)
149
+
150
+ year, month, day = *header.unpack('@1ccc')
151
+ year += 2000 if year >= 100
152
+
153
+ @last_modified_on = Date.new(year, month, day)
154
+ @count = header.unpack('@4V').first
155
+ @language = header.unpack('@29c').first
156
+
157
+ @record_offset = *header.unpack('@8v')
158
+ @record_size = *header.unpack('@10v')
159
+
160
+ @file.pos = 32
161
+
162
+ @columns = []
163
+ @name_to_columns = {}
164
+ while true do
165
+ column_data = @file.read(32)
166
+ break if column_data[0, 1] == "\x0d"
167
+ name, type, offset, size, decimal = *column_data.unpack('@0a11aLCC')
168
+ name = name.strip
169
+ @columns << Columns::Column.column_for(type).new(name, :offset => offset, :size => size, :decimal => decimal)
170
+ @name_to_columns[name.upcase.to_sym] = @columns.last
171
+ end
172
+ end
173
+
174
+ def save(record)
175
+ if !record.index
176
+ @file.pos = @record_offset + @record_size*count
177
+ @file.write record.serialize
178
+ @file.write [26].pack('c')
179
+ self.count += 1
180
+ else
181
+ throw "Index out of bound" if index>=count
182
+ @file.pos = @record_offset + @record_size*index
183
+ @file.write record.serialize
184
+ end
185
+ update_header
186
+ end
187
+
188
+ def count=(value)
189
+ @count = value
190
+ end
191
+
192
+ def last_modified_on=(value)
193
+ @last_modified_on = value
194
+ end
195
+
196
+ def update_header
197
+ @last_modified_on = Date.today
198
+ @file.pos = 1
199
+ @file.write([last_modified_on.year % 100, last_modified_on.month, last_modified_on.day].pack('ccc'))
200
+ @file.write([count].pack('V'))
201
+ end
202
+ end
203
+
204
+ end
metadata ADDED
@@ -0,0 +1,54 @@
1
+ --- !ruby/object:Gem::Specification
2
+ rubygems_version: 0.8.11
3
+ specification_version: 1
4
+ name: rbase
5
+ version: !ruby/object:Gem::Version
6
+ version: "0.1"
7
+ date: 2006-09-11 00:00:00 +04:00
8
+ summary: Library to create/read/write to XBase databases (*.DBF files)
9
+ require_paths:
10
+ - lib
11
+ email: maxim.kulkin@gmail.com, leonardo.pires@gmail.com
12
+ homepage: http://rbase.rubyforge.com/
13
+ rubyforge_project: rbase
14
+ description:
15
+ autorequire: rbase
16
+ default_executable:
17
+ bindir: bin
18
+ has_rdoc: true
19
+ required_ruby_version: !ruby/object:Gem::Version::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.8.2
24
+ version:
25
+ platform: ruby
26
+ signing_key:
27
+ cert_chain:
28
+ authors:
29
+ - Maxim Kulkin, Leonardo Augusto Pires
30
+ files:
31
+ - Rakefile
32
+ - lib
33
+ - lib/rbase
34
+ - lib/rbase.rb
35
+ - lib/rbase/schema_dumper.rb
36
+ - lib/rbase/record.rb
37
+ - lib/rbase/columns.rb
38
+ - lib/rbase/schema.rb
39
+ - lib/rbase/builder.rb
40
+ - lib/rbase/table.rb
41
+ test_files: []
42
+
43
+ rdoc_options: []
44
+
45
+ extra_rdoc_files: []
46
+
47
+ executables: []
48
+
49
+ extensions: []
50
+
51
+ requirements: []
52
+
53
+ dependencies: []
54
+