rbase-ff 0.9

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.
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: []