rbase-ff 0.9

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 43fb9c0a12fadbc618c2447b8a3d9f67e30cc4b0
4
+ data.tar.gz: 8b9e1657d06949598eb524d32d844c7bf1a0e773
5
+ SHA512:
6
+ metadata.gz: 0b6f2fc63244d75a344b963ce63a9b03b1b446db53fc5e72144770a8fd01f855e64c49f8817b62cfa3541b369fe13c3da31f5fd184b7129f4650652fba2c1b75
7
+ data.tar.gz: 2d540336e920c3e05b286d452afcfbaf07989f66944edb5d3e023186f8396f4b82f0608e0cae12e22c5c2b3725b333d5f2d40d3879aac9c71d24f490fe6f71b6
data/README.md ADDED
@@ -0,0 +1,27 @@
1
+ rbase
2
+ =====
3
+
4
+ rbase gem (working with dbf files) for ruby 1.9.3 (iconv removed, some fixes)
5
+
6
+ How to use
7
+ ==========
8
+
9
+ ```ruby
10
+ require 'rubygems'
11
+ require "date"
12
+ require "rbase"
13
+
14
+ RBase.create_table 'people' do |t|
15
+ t.column :name, :string, :size => 30
16
+ t.column :birthdate, :date
17
+ t.column :active, :boolean
18
+ t.column :tax, :integer, :size => 10, :decimal => 2
19
+ end
20
+
21
+ RBase::Table.open 'people' do |t|
22
+ t.create name: 'People-1', birthdate: Date.new(2017,1,2), active: true, tax: 5.2
23
+ t.create name: 'People-2', birthdate: Date.new(2018,1,2), active: true, tax: 6.2
24
+ t.create name: 'People-3', birthdate: Date.new(2019,1,2), active: true, tax: 7.2
25
+ t.create name: 'People-4', birthdate: Date.new(2020,1,2), active: true, tax: 8.2
26
+ end
27
+ ```
@@ -0,0 +1,34 @@
1
+ require 'rbase/schema'
2
+
3
+ module RBase
4
+ LANGUAGE_US_DOS = 0x01
5
+ LANGUAGE_INTL_DOS = 0x02
6
+ LANGUAGE_ANSI_WINDOWS = 0x03
7
+ LANGUAGE_RUSSIAN_DOS = 0x66
8
+ LANGUAGE_RUSSIAN_WINDOWS = 0xc9
9
+
10
+ # Create new XBase table file. Table file name will be equal to name with ".dbf" suffix.
11
+ #
12
+ # For list of available options see Table::create documentation.
13
+ #
14
+ # == Example
15
+ #
16
+ # RBase.create_table 'people' do |t|
17
+ # t.column :name, :string, :size => 30
18
+ # t.column :birthdate, :date
19
+ # t.column :active, :boolean
20
+ # t.column :tax, :integer, :size => 10, :decimal => 2
21
+ # end
22
+ #
23
+ # For documentation on column parameters see RBase::Schema.column documentation.
24
+ #
25
+ def self.create_table(name, options = {})
26
+ options[:language] ||= LANGUAGE_RUSSIAN_WINDOWS
27
+
28
+ schema = Schema.new
29
+ yield schema if block_given?
30
+
31
+ Table.create name, schema, options
32
+ end
33
+
34
+ end
@@ -0,0 +1,251 @@
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
+
47
+ def attach_to(table)
48
+ @table = table
49
+ end
50
+
51
+ # Packs column value for storing it in XBase file.
52
+ def pack(value)
53
+ throw "Not implemented"
54
+ end
55
+
56
+ # Unpacks stored in XBase column data into appropriate Ruby form.
57
+ def unpack(value)
58
+ throw "Not implemented"
59
+ end
60
+
61
+ def inspect
62
+ "#{name}(type=#{type}, size=#{size})"
63
+ end
64
+
65
+ protected
66
+
67
+ def table
68
+ @table
69
+ end
70
+ end
71
+
72
+
73
+ class CharacterColumn < Column
74
+ column_type 'C'
75
+
76
+ def initialize(name, options = {})
77
+ if options[:size] && options[:decimal]
78
+ size = options[:decimal]*256 + options[:size]
79
+ else
80
+ size = options[:size] || 254
81
+ end
82
+
83
+ super name, options.merge(:size => size)
84
+
85
+ if options[:encoding]
86
+ @unpack_converter = Encoder.new(options[:encoding], 'utf-8')
87
+ @pack_converter = Encoder.new('utf-8', options[:encoding])
88
+ end
89
+ end
90
+
91
+ def pack(value)
92
+ value = value.to_s
93
+ value = @pack_converter.en(value) if @pack_converter
94
+ [value].pack("A#{size}")
95
+ end
96
+
97
+ def unpack(data)
98
+ value = data.rstrip
99
+ value = @unpack_converter.en(value) if @unpack_converter
100
+ value
101
+ end
102
+
103
+ def inspect
104
+ "#{name}(string #{size})"
105
+ end
106
+ end
107
+
108
+
109
+ class NumberColumn < Column
110
+ column_type 'N'
111
+
112
+ def initialize(name, options = {})
113
+ size = options[:size] || 18
114
+ size = 18 if size > 18
115
+
116
+ super name, options.merge(:size => size)
117
+ end
118
+
119
+ def pack(value)
120
+ if value
121
+ if float?
122
+ [format("%#{size-decimal-1}.#{decimal}f", value)].pack("A#{size}")
123
+ else
124
+ [format("%#{size}d", value)].pack("A#{size}")
125
+ end
126
+ else
127
+ " "*size
128
+ end
129
+ end
130
+
131
+ def unpack(data)
132
+ return nil if data.strip == ''
133
+ data.rstrip.to_i
134
+ end
135
+
136
+ def inspect
137
+ if float?
138
+ "#{name}(decimal)"
139
+ else
140
+ "#{name}(integer)"
141
+ end
142
+ end
143
+
144
+ def float?
145
+ decimal && decimal != 0
146
+ end
147
+ end
148
+
149
+
150
+ class LogicalColumn < Column
151
+ column_type 'L'
152
+
153
+ def initialize(name, options = {})
154
+ super name, options.merge(:size => 1)
155
+ end
156
+
157
+ def pack(value)
158
+ case value
159
+ when true then 'T'
160
+ when false then 'F'
161
+ else '?'
162
+ end
163
+ end
164
+
165
+ def unpack(data)
166
+ case data.upcase
167
+ when 'Y', 'T'
168
+ true
169
+ when 'N', 'F'
170
+ false
171
+ else
172
+ nil
173
+ end
174
+ end
175
+
176
+ def inspect
177
+ "#{name}(boolean)"
178
+ end
179
+ end
180
+
181
+
182
+ class DateColumn < Column
183
+ column_type 'D'
184
+
185
+ def initialize(name, options = {})
186
+ super name, options.merge(:size => 8)
187
+ end
188
+
189
+ def pack(value)
190
+ value ? value.strftime('%Y%m%d'): ' '*8
191
+ end
192
+
193
+ def unpack(data)
194
+ return nil if data.rstrip == ''
195
+ Date.new(*data.unpack("a4a2a2").map { |s| s.to_i})
196
+ end
197
+
198
+ def inspect
199
+ "#{name}(date)"
200
+ end
201
+ end
202
+
203
+
204
+ class MemoColumn < Column
205
+ column_type 'M'
206
+
207
+ def initialize(name, options = {})
208
+ super name, options.merge(:size => 10)
209
+ end
210
+
211
+ def pack(value)
212
+ packed_value = table.memo.write(value)
213
+ [format("%-10d", packed_value)].pack('A10')
214
+ end
215
+
216
+ def unpack(data)
217
+ table.memo.read(data.to_i)
218
+ end
219
+
220
+ def inspect
221
+ "#{name}(memo)"
222
+ end
223
+ end
224
+
225
+
226
+ class FloatColumn < Column
227
+ column_type 'F'
228
+
229
+ def initialize(name, options = {})
230
+ super name, options.merge(:size => 20)
231
+ end
232
+
233
+ def decimal
234
+ (@decimal && @decimal <= 15) ? @decimal : 2
235
+ end
236
+
237
+ def pack(value)
238
+ [format("%-#{size-decimal-1}.#{decimal}f", value || 0.0)].pack("A#{size}")
239
+ end
240
+
241
+ def unpack(data)
242
+ data.rstrip.to_f
243
+ end
244
+
245
+ def inspect
246
+ "#{name}(float)"
247
+ end
248
+ end
249
+
250
+ end
251
+ end
@@ -0,0 +1,11 @@
1
+ class Encoder
2
+
3
+ def initialize(from, to)
4
+ @from, @to = from, to
5
+ end
6
+
7
+ def en(str)
8
+ str.force_encoding(@from).encode(@to)
9
+ end
10
+
11
+ end
@@ -0,0 +1,63 @@
1
+ module RBase
2
+ module MemoFile
3
+
4
+ class DummyMemoFile
5
+ def read(index)
6
+ ''
7
+ end
8
+
9
+ def write(value)
10
+ nil
11
+ end
12
+ end
13
+
14
+ class DBase3MemoFile
15
+ HEADER_SIZE = 512
16
+ BLOCK_SIZE = 512
17
+ BLOCK_TERMINATOR = "\x1a\x1a"
18
+
19
+ def initialize(name)
20
+ @file = File.open(name)
21
+
22
+ @header = @file.read(HEADER_SIZE)
23
+ @next_block = header.unpack('@0L')
24
+ @version = header.unpack('@16c')
25
+ end
26
+
27
+ def read(index)
28
+ @file.pos = index*BLOCK_SIZE + HEADER_SIZE
29
+
30
+ result = ''
31
+ loop do
32
+ data = @file.read(BLOCK_SIZE)
33
+ terminator_pos = data.index(BLOCK_TERMINATOR)
34
+ if terminator_pos
35
+ break result + data[0, terminator_pos]
36
+ end
37
+ result += data
38
+ end
39
+ end
40
+
41
+ def write(value)
42
+ @file.pos = @next_block*BLOCK_SIZE + HEADER_SIZE
43
+ value += BLOCK_TERMINATOR
44
+ blocks_num = (value.length+511)/512
45
+ @file.write [value].pack("a#{512*blocks_num}")
46
+
47
+ position = @next_block
48
+ @next_block += blocks_num
49
+ update_header
50
+
51
+ position
52
+ end
53
+
54
+ protected
55
+
56
+ def update_header
57
+ @file.pos = 0
58
+ @file.write [@next_block].pack("L")
59
+ end
60
+ end
61
+
62
+ end
63
+ end
@@ -0,0 +1,141 @@
1
+ module RBase
2
+
3
+ class StandardError < Exception; end
4
+
5
+ class UnknownColumnError < StandardError
6
+ attr_reader :name
7
+
8
+ def initialize(name)
9
+ super("Unknown column '#{name}'")
10
+ @name = name
11
+ end
12
+ end
13
+
14
+ class InvalidValueError < StandardError
15
+ attr_reader :column, :value
16
+
17
+ def initialize(column, value)
18
+ super("Invalid value #{value.inspect} for column #{column.inspect}")
19
+ @column, @value = column, value
20
+ end
21
+ end
22
+
23
+ # Class that contains data for particular table row.
24
+ # Should not be created explicitly (use Table#create to create records)
25
+ #
26
+ # == Accessing attributes
27
+ #
28
+ # You can read and assign values to row's columns using simple property syntax:
29
+ #
30
+ # user = users_table[0]
31
+ # user.name = 'Bob'
32
+ # user.birth_date = Date.new(1980, 2, 29)
33
+ # user.save
34
+ #
35
+ # puts user.name
36
+ # puts user.birth_date
37
+ #
38
+ class Record
39
+ attr_reader :table, :index
40
+
41
+ def initialize(table, attributes = {})
42
+ @table = table
43
+ @values_cached = {}
44
+ @values_changed = {}
45
+
46
+ attributes.each { |k, v| @values_changed[k.to_s.upcase] = v }
47
+ end
48
+
49
+ private
50
+
51
+ def load(index, data)
52
+ @table = table
53
+ @index = index
54
+ @data = data.dup
55
+ end
56
+
57
+ public
58
+
59
+ # Returns true if record was never saved to database; otherwise return false.
60
+ def new_record?
61
+ @data.nil?
62
+ end
63
+
64
+ # Save record to database.
65
+ def save
66
+ record = self
67
+ @table.instance_eval { save(record) }
68
+ end
69
+
70
+ # Delete record from database.
71
+ def delete
72
+ @deleted = true
73
+ save
74
+ end
75
+
76
+ # Returns true if record was marked as deleted; otherwise return false.
77
+ def deleted?
78
+ @deleted ||= new_record? ? false : @data[0, 1] == '*'
79
+ end
80
+
81
+ # Clone record.
82
+ def clone
83
+ c = self.class.new(@table, @values_changed)
84
+ c.instance_variable_set("@values_cached", @values_cached)
85
+ c.instance_variable_set("@data", @data)
86
+ c
87
+ end
88
+
89
+ def method_missing(sym, *args)
90
+ name = sym.to_s
91
+ if /=$/ =~ name && args.size == 1
92
+ set_value(name[0..-2], args.first)
93
+ else
94
+ get_value(name)
95
+ end
96
+ end
97
+
98
+ def serialize
99
+ if new_record?
100
+ @data = deleted? ? '*' : ' '
101
+ @data << @table.columns.collect do |column|
102
+ column.pack(@values_changed[column.name])
103
+ end.join
104
+ else
105
+ @data[0, 1] = deleted? ? '*' : ' '
106
+ @values_changed.each do |k, v|
107
+ column = @table.column(k)
108
+ raise UnknownColumnError.new(k) unless column
109
+ begin
110
+ @data[column.offset, column.size] = column.pack(v)
111
+ rescue Object => e
112
+ raise InvalidValueError.new(column, v)
113
+ end
114
+ @values_cached[k] = v
115
+ end
116
+ end
117
+ @data
118
+ end
119
+
120
+ protected
121
+
122
+ # Returns value of specified column
123
+ def get_value(name)
124
+ name = name.to_s.upcase.to_sym
125
+ return @values_changed[name] if @values_changed.has_key?(name)
126
+ return nil if new_record?
127
+ column = @table.column(name)
128
+ raise UnknownColumnError.new(name) unless column
129
+ @values_cached[name] ||= column.unpack(@data[column.offset, column.size])
130
+ end
131
+
132
+ # Sets value of specified column.
133
+ def set_value(name, value)
134
+ name = name.to_s.upcase.to_sym
135
+ raise UnknownColumnError.new(name) unless @table.column(name)
136
+ @values_changed[name] = value
137
+ end
138
+ end
139
+
140
+ end
141
+
@@ -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 RBase::Table opened
8
+ #
9
+ # == Example
10
+ #
11
+ # users = RBase::Table.open('users')
12
+ # File.open('users.dump.rb', 'w') do |f|
13
+ # f.write RBase::SchemaDumper.dump(users)
14
+ # end
15
+ # users.close
16
+ #
17
+ def self.dump(table)
18
+ output = ''
19
+ output << "RBase.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,239 @@
1
+ module RBase
2
+
3
+ class Table
4
+ private_class_method :new
5
+
6
+ include Enumerable
7
+
8
+ # Create new XBase table file. Table file name will be equal to name with ".dbf" suffix.
9
+ #
10
+ # Allowed options
11
+ # * :language - language character set used in database. Can be one of LANGUAGE_* constants
12
+ def self.create(name, schema, options = {})
13
+ date = Date.today
14
+
15
+ record_size = 1+schema.columns.inject(0) { |size, column| size + column.size }
16
+
17
+ data = ''
18
+ # data << [0xf5].pack('C') # version
19
+ data << [0x3].pack('C') # version
20
+ data << [date.year % 100, date.month, date.day].pack('CCC') # last modification date
21
+ data << [0].pack('L') # number of records
22
+ # data << [32+schema.columns.size*32+263+1].pack('v') # data size
23
+ data << [32+schema.columns.size*32+1].pack('v') # data size
24
+ data << [record_size].pack('v') # record size
25
+ data << [].pack('x2') # reserved
26
+ data << [].pack('x') # incomplete transaction
27
+ data << [].pack('x') # encyption flag
28
+ data << [].pack('x4') # reserved
29
+ data << [].pack('x8') # reserved
30
+ data << [0].pack('c') # mdx flag
31
+ data << [options[:language]].pack('C') # language driver
32
+ data << [].pack('x2') # reserved
33
+
34
+ offset = 1 # take into account 1 byte for deleted flag
35
+ data << schema.columns.collect do |column|
36
+ s = ''
37
+ s << [column.name.to_s[0..9]].pack('a11') # field name
38
+ s << [column.type].pack('a') # field type
39
+ s << [offset].pack('L') # field data offset
40
+ s << [column.size].pack('C') # field size
41
+ s << [column.decimal || 0].pack('C') # decimal count
42
+ s << [].pack('x2') # reserved
43
+ s << [].pack('x') # work area id
44
+ s << [].pack('x2') # reserved
45
+ s << [].pack('x') # flag for SET FIELDS
46
+ s << [].pack('x7') # reserved
47
+ s << [].pack('x') # index field flag
48
+ offset += column.size
49
+ s
50
+ end.join
51
+
52
+ data << [13].pack('C') # terminator
53
+
54
+ data << [26].pack('C') # end of file
55
+
56
+ File.open("#{name}.dbf", 'wb') do |f|
57
+ f.write data
58
+ end
59
+ end
60
+
61
+ # Open table with given name.
62
+ # Table name should be like file name without ".dbf" suffix.
63
+ def self.open(name, options = {})
64
+ table = new
65
+ table.instance_eval { open("#{name}.dbf", options) }
66
+ if block_given?
67
+ result = yield table
68
+ table.close
69
+ result
70
+ else
71
+ table
72
+ end
73
+ end
74
+
75
+ # Physically remove records that were marked as deleted from file.
76
+ def pack
77
+ packed_count = 0
78
+ count.times do |i|
79
+ @file.pos = @record_offset + @record_size*i
80
+ data = @file.read(@record_size)
81
+ unless data[0, 1]=='*'
82
+ if i!=packed_count
83
+ @file.pos = @record_offset + @record_size*packed_count
84
+ @file.write data
85
+ end
86
+ packed_count += 1
87
+ end
88
+ end
89
+
90
+ file_end = @record_offset + @record_size*packed_count
91
+ @file.pos = file_end
92
+ @file.write "\x1a"
93
+ @file.truncate file_end+1
94
+
95
+ self.count = packed_count
96
+ update_header
97
+ end
98
+
99
+ def clear
100
+ file_end = @record_offset
101
+ @file.pos = file_end
102
+ @file.write "\x1a"
103
+ @file.truncate file_end+1
104
+
105
+ self.count = 0
106
+ update_header
107
+ end
108
+
109
+ def close
110
+ @file.close
111
+ end
112
+
113
+ attr_reader :name, :count, :columns, :last_modified_on, :language
114
+
115
+ # Return instance of RBase::Column for given column name
116
+ def column(name)
117
+ @name_to_columns[name]
118
+ end
119
+
120
+ # Returns instance of MemoFile that is associated with table
121
+ def memo
122
+ return @memo_file
123
+ end
124
+
125
+ # Create new record, populate it with given attributes
126
+ def build(attributes = {})
127
+ Record.new(self, attributes)
128
+ end
129
+
130
+ # Create new record, populate it with given attributes and save it
131
+ def create(attributes = {})
132
+ record = build(attributes)
133
+ record.save
134
+ record
135
+ end
136
+
137
+ # Load record stored in position 'index'
138
+ def load(index)
139
+ @file.pos = @record_offset + @record_size*index
140
+ data = @file.read(@record_size)
141
+ record = Record.new(self)
142
+ record.instance_eval { load(index, data) }
143
+ record
144
+ end
145
+
146
+ alias_method :[], :load
147
+
148
+ def []=(index, record)
149
+ record.instance_eval { @index = index }
150
+ save(record)
151
+ end
152
+
153
+ # Iterate through all (even deleted) records
154
+ def each_with_deleted
155
+ return unless block_given?
156
+
157
+ count.times do |i|
158
+ yield load(i)
159
+ end
160
+ end
161
+
162
+ # Iterate through all non-deleted records
163
+ def each
164
+ return unless block_given?
165
+
166
+ self.each_with_deleted do |record|
167
+ yield record unless record.deleted?
168
+ end
169
+ end
170
+
171
+ private
172
+
173
+ def open(name, options = {})
174
+ @name = File.basename(name, '.dbf')
175
+ @file = File.open(name, "r+b")
176
+ header = @file.read(32)
177
+
178
+ year, month, day = *header.unpack('@1ccc')
179
+ year += 2000 if year >= 100
180
+
181
+ @last_modified_on = Date.new(year, month, day)
182
+ @count = header.unpack('@4V').first
183
+ @language = header.unpack('@29c').first
184
+
185
+ @record_offset = header.unpack('@8v').first
186
+ @record_size = header.unpack('@10v').first
187
+
188
+ @file.pos = 32
189
+
190
+ @columns = []
191
+ @name_to_columns = {}
192
+ column_options = {}
193
+ column_options[:encoding] = options[:encoding] if options[:encoding]
194
+ while true do
195
+ column_data = @file.read(32)
196
+ break if column_data[0, 1] == "\x0d"
197
+ name, type, offset, size, decimal = *column_data.unpack('@0a11aLCC')
198
+ name = name.strip
199
+ @columns << Columns::Column.column_for(type).new(name, options.merge(:offset => offset, :size => size, :decimal => decimal))
200
+ @name_to_columns[name.upcase.to_sym] = @columns.last
201
+ end
202
+
203
+ @columns.each { |column| column.attach_to(self) }
204
+
205
+ @memo_file = MemoFile::DummyMemoFile.new
206
+ end
207
+
208
+ def save(record)
209
+ if !record.index
210
+ @file.pos = @record_offset + @record_size*count
211
+ @file.write record.serialize
212
+ @file.write [26].pack('c')
213
+ record.instance_variable_set(:@index, count)
214
+ self.count = count + 1
215
+ else
216
+ throw "Index out of bound" if record.index>=count
217
+ @file.pos = @record_offset + @record_size*record.index
218
+ @file.write record.serialize
219
+ end
220
+ update_header
221
+ end
222
+
223
+ def count=(value)
224
+ @count = value
225
+ end
226
+
227
+ def last_modified_on=(value)
228
+ @last_modified_on = value
229
+ end
230
+
231
+ def update_header
232
+ @last_modified_on = Date.today
233
+ @file.pos = 1
234
+ @file.write([last_modified_on.year % 100, last_modified_on.month, last_modified_on.day].pack('ccc'))
235
+ @file.write([count].pack('V'))
236
+ end
237
+ end
238
+
239
+ end
data/lib/rbase.rb ADDED
@@ -0,0 +1,8 @@
1
+ require 'rbase/encoder'
2
+ require 'rbase/columns'
3
+ require 'rbase/schema'
4
+ require 'rbase/memo_file'
5
+ require 'rbase/table'
6
+ require 'rbase/record'
7
+ require 'rbase/schema_dumper'
8
+ require 'rbase/builder'
data/rbase.gemspec ADDED
@@ -0,0 +1,13 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'rbase-ff'
3
+ s.version = '0.9'
4
+ s.summary = 'Library to create/read/write to XBase databases (*.DBF files)'
5
+ s.require_path = 'lib'
6
+ s.files = Dir.glob('**/*').delete_if { |item| item =~ /^\./ }
7
+ s.authors = 'Maxim Kulkin, Leonardo Augusto Pires, Tom Lahti'
8
+ s.email = 'maxim.kulkin@gmail.com, leonardo.pires@gmail.com, uidzip@gmail.com'
9
+ s.homepage = 'http://github.com/uidzip/rbase'
10
+ s.has_rdoc = true
11
+
12
+ s.required_ruby_version = '>= 1.9.3'
13
+ end
metadata ADDED
@@ -0,0 +1,53 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rbase-ff
3
+ version: !ruby/object:Gem::Version
4
+ version: '0.9'
5
+ platform: ruby
6
+ authors:
7
+ - Maxim Kulkin, Leonardo Augusto Pires, Tom Lahti
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-11 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description:
14
+ email: maxim.kulkin@gmail.com, leonardo.pires@gmail.com, uidzip@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - README.md
20
+ - lib/rbase.rb
21
+ - lib/rbase/builder.rb
22
+ - lib/rbase/columns.rb
23
+ - lib/rbase/encoder.rb
24
+ - lib/rbase/memo_file.rb
25
+ - lib/rbase/record.rb
26
+ - lib/rbase/schema.rb
27
+ - lib/rbase/schema_dumper.rb
28
+ - lib/rbase/table.rb
29
+ - rbase.gemspec
30
+ homepage: http://github.com/uidzip/rbase
31
+ licenses: []
32
+ metadata: {}
33
+ post_install_message:
34
+ rdoc_options: []
35
+ require_paths:
36
+ - lib
37
+ required_ruby_version: !ruby/object:Gem::Requirement
38
+ requirements:
39
+ - - ">="
40
+ - !ruby/object:Gem::Version
41
+ version: 1.9.3
42
+ required_rubygems_version: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ requirements: []
48
+ rubyforge_project:
49
+ rubygems_version: 2.6.11
50
+ signing_key:
51
+ specification_version: 4
52
+ summary: Library to create/read/write to XBase databases (*.DBF files)
53
+ test_files: []