rbase 0.1

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