tb 0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,35 @@
1
+ # lib/tb.rb - entry file for table library
2
+ #
3
+ # Copyright (C) 2010-2011 Tanaka Akira <akr@fsij.org>
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice, this
9
+ # list of conditions and the following disclaimer.
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # 3. The name of the author may not be used to endorse or promote products
14
+ # derived from this software without specific prior written permission.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
21
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
24
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
25
+ # OF SUCH DAMAGE.
26
+
27
+ require 'tb/basic'
28
+ require 'tb/record'
29
+ require 'tb/csv'
30
+ require 'tb/tsv'
31
+ require 'tb/qtsv'
32
+ require 'tb/reader'
33
+ require 'tb/fieldset'
34
+ require 'tb/pathfinder'
35
+ require 'tb/enumerable'
@@ -0,0 +1,1071 @@
1
+ # lib/tb/basic.rb - basic fetures for table library
2
+ #
3
+ # Copyright (C) 2010-2011 Tanaka Akira <akr@fsij.org>
4
+ #
5
+ # Redistribution and use in source and binary forms, with or without
6
+ # modification, are permitted provided that the following conditions are met:
7
+ #
8
+ # 1. Redistributions of source code must retain the above copyright notice, this
9
+ # list of conditions and the following disclaimer.
10
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
11
+ # this list of conditions and the following disclaimer in the documentation
12
+ # and/or other materials provided with the distribution.
13
+ # 3. The name of the author may not be used to endorse or promote products
14
+ # derived from this software without specific prior written permission.
15
+ #
16
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
17
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
18
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
19
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
20
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
21
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
24
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
25
+ # OF SUCH DAMAGE.
26
+
27
+ require 'pp'
28
+
29
+ # Tb represents a set of records.
30
+ # A record contains field values accessed by field names.
31
+ #
32
+ # A table can be visualized as follows.
33
+ #
34
+ # _recordid f1 f2 f3
35
+ # 0 v01 v02 v03
36
+ # 1 v11 v12 v13
37
+ # 2 v21 v22 v23
38
+ #
39
+ # This table has 4 fields and 3 records:
40
+ # - fields: _recordid, f1, f2 and f3.
41
+ # - records: [0, v01, v02, v03], [1, v11, v12, v13] and [2, v21, v22, v23]
42
+ #
43
+ # The fields are strings.
44
+ # The field names starts with "_" is reserved.
45
+ # "_recordid" is a reserved field always defined to identify a record.
46
+ #
47
+ # Non-reserved fields can be defined by Tb.new and Tb#define_field.
48
+ # It is an error to access a field which is not defined.
49
+ #
50
+ # A value in a record is identified by a recordid and field name.
51
+ # A value for non-reserved fields can be any Ruby values.
52
+ # A value for _recordid is an non-negative integer and it is automatically allocated when a new record is inserted.
53
+ # It is an error to access a record by recordid which is not allocated.
54
+ #
55
+ class Tb
56
+ include Enumerable
57
+
58
+ # :call-seq:
59
+ # Tb.new
60
+ # Tb.new(fields, values1, values2, ...)
61
+ #
62
+ # creates an instance of Tb class.
63
+ #
64
+ # If the first argument, _fields_, is given, it should be an array of strings.
65
+ # The strings are used as field names to define fields.
66
+ #
67
+ # The field names begins with underscore, "_", are reserved.
68
+ # Currently, "_recordid" field is defined automatically.
69
+ #
70
+ # If the second argument and subsequent arguments, valuesN, are given, they should be an array.
71
+ # The arrays are used as records to define records.
72
+ # A value in the array is used for a value of corresponding field defined by the first argument.
73
+ #
74
+ # t = Tb.new %w[fruit color],
75
+ # %w[apple red],
76
+ # %w[banana yellow],
77
+ # %w[orange orange]
78
+ # pp t
79
+ # #=> #<Tb
80
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
81
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}>
82
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
83
+ #
84
+ def initialize(*args)
85
+ @next_recordid = 0
86
+ @recordid2index = {}
87
+ @free_index = []
88
+ @tbl = {"_recordid"=>[]}
89
+ @field_list = ["_recordid".freeze]
90
+ if !args.empty?
91
+ args.first.each {|f|
92
+ define_field(f)
93
+ }
94
+ insert_values(*args)
95
+ end
96
+ end
97
+
98
+ # :call-seq:
99
+ # table.replace(table2)
100
+ #
101
+ # replaces the contents of _table_ same as _table2_.
102
+ def replace(tbl2)
103
+ raise TypeError, "a Tb expected but #{tbl2.inspect}" unless Tb === tbl2
104
+ @next_recordid = tbl2.instance_variable_get(:@next_recordid)
105
+ @recordid2index = tbl2.instance_variable_get(:@recordid2index).dup
106
+ @free_index = tbl2.instance_variable_get(:@free_index).dup
107
+ @tbl = Hash[tbl2.instance_variable_get(:@tbl).map {|k, v| [k, v.dup] }]
108
+ @field_list = tbl2.instance_variable_get(:@field_list).dup
109
+ end
110
+
111
+ def pretty_print(q) # :nodoc:
112
+ q.object_group(self) {
113
+ each_recordid {|recordid|
114
+ q.breakable
115
+ fs = @field_list.reject {|f| get_cell(recordid, f).nil? }
116
+ q.group(1, '{', '}') {
117
+ q.seplist(fs, nil, :each) {|f|
118
+ v = get_cell(recordid, f)
119
+ q.group {
120
+ q.pp f
121
+ q.text '=>'
122
+ q.group(1) {
123
+ q.breakable ''
124
+ q.pp v
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ }
131
+ end
132
+ alias inspect pretty_print_inspect # :nodoc:
133
+
134
+ def check_recordid_type(recordid)
135
+ raise TypeError, "invalid recordid: #{recordid.inspect}" if recordid.kind_of?(Symbol) # Ruby 1.8 has Symbol#to_int.
136
+ raise TypeError, "invalid recordid: #{recordid.inspect}" unless recordid.respond_to? :to_int
137
+ recordid = recordid.to_int
138
+ raise TypeError, "invalid recordid: #{recordid.inspect}" if !recordid.kind_of?(Integer)
139
+ recordid
140
+ end
141
+ private :check_recordid_type
142
+
143
+ def check_recordid(recordid)
144
+ recordid = check_recordid_type(recordid)
145
+ if !@recordid2index.include?(recordid)
146
+ raise IndexError, "unexpected recordid: #{recordid.inspect}"
147
+ end
148
+ recordid
149
+ end
150
+ private :check_recordid
151
+
152
+ def check_field_type(field)
153
+ raise TypeError, "invalid field name: #{field.inspect}" if field.nil?
154
+ field = field.to_s
155
+ raise TypeError, "invalid field name: #{field.inspect}" if !field.kind_of?(String)
156
+ field
157
+ end
158
+ private :check_field_type
159
+
160
+ def check_field(field)
161
+ field = check_field_type(field)
162
+ unless @tbl.include? field
163
+ raise ArgumentError, "field not defined: #{field.inspect}"
164
+ end
165
+ field
166
+ end
167
+ private :check_field
168
+
169
+ # :call-seq:
170
+ # table.define_field(field)
171
+ # table.define_field(field) {|record| value_for_the_field }
172
+ #
173
+ # defines a new field.
174
+ #
175
+ # If no block is given, the initial value for the field is nil.
176
+ #
177
+ # If a block is given, the block is called for each record.
178
+ # The return value of the block is used for the initial value of the field.
179
+ #
180
+ # t = Tb.new %w[fruit color],
181
+ # %w[apple red],
182
+ # %w[banana yellow],
183
+ # %w[orange orange]
184
+ # pp t
185
+ # #=> #<Tb
186
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
187
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
188
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
189
+ # t.define_field("namelen") {|record| record["fruit"].length }
190
+ # pp t
191
+ # #=> #<Tb
192
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red", "namelen"=>5}
193
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow", "namelen"=>6}
194
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange", "namelen"=>6}>
195
+ #
196
+ def define_field(field)
197
+ field = check_field_type(field).dup.freeze
198
+ if field.start_with?("_")
199
+ raise ArgumentError, "field begins with underscore: #{field.inspect}"
200
+ end
201
+ if @tbl.include? field
202
+ raise ArgumentError, "field already defined: #{field.inspect}"
203
+ end
204
+ @tbl[field] = []
205
+ @field_list << field
206
+ if block_given?
207
+ each_record {|record|
208
+ v = yield(record)
209
+ if !v.nil?
210
+ record[field] = v
211
+ end
212
+ }
213
+ end
214
+ end
215
+
216
+ # :call-seq:
217
+ # table.has_field?(field) -> true or false
218
+ #
219
+ # returns true if the field specified by the argument is exist.
220
+ #
221
+ # t = Tb.new %w[fruit color],
222
+ # %w[apple red],
223
+ # %w[banana yellow],
224
+ # %w[orange orange]
225
+ # pp t
226
+ # #=> #<Tb
227
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
228
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
229
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
230
+ # p t.has_field?("fruit") #=> true
231
+ # p t.has_field?("foo") #=> false
232
+ #
233
+ def has_field?(field)
234
+ field = check_field_type(field)
235
+ @tbl.has_key?(field)
236
+ end
237
+
238
+ # :call-seq:
239
+ # table.list_fields -> [field1, field2, ...]
240
+ #
241
+ # returns the list of non-reserved field names as an array of strings.
242
+ #
243
+ # t = Tb.new %w[fruit color],
244
+ # %w[apple red],
245
+ # %w[banana yellow],
246
+ # %w[orange orange]
247
+ # pp t
248
+ # #=> #<Tb
249
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
250
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
251
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
252
+ # p t.list_fields #=> ["fruit", "color"]
253
+ #
254
+ def list_fields
255
+ @field_list.reject {|f| f.start_with?("_") }
256
+ end
257
+
258
+ # :call-seq:
259
+ # table.list_fields_all -> [field1, field2, ...]
260
+ #
261
+ # returns the list of reserved and non-reserved field names as an array of strings.
262
+ #
263
+ # t = Tb.new %w[fruit color],
264
+ # %w[apple red],
265
+ # %w[banana yellow],
266
+ # %w[orange orange]
267
+ # pp t
268
+ # #=> #<Tb
269
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
270
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
271
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
272
+ # p t.list_fields_all #=> ["_recordid", "fruit", "color"]
273
+ #
274
+ def list_fields_all
275
+ @field_list.dup
276
+ end
277
+
278
+ # :call-seq:
279
+ # table.reorder_fields!(fields)
280
+ #
281
+ # reorder the fields.
282
+ #
283
+ # t = Tb.new %w[fruit color],
284
+ # %w[apple red],
285
+ # %w[banana yellow],
286
+ # %w[orange orange]
287
+ # p t.list_fields
288
+ # #=> ["fruit", "color"]
289
+ # pp t
290
+ # #=> #<Tb
291
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
292
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
293
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
294
+ # t.reorder_fields! %w[color fruit]
295
+ # p t.list_fields
296
+ # #=> ["color", "fruit"]
297
+ # pp t
298
+ # #=> #<Tb
299
+ # # {"_recordid"=>0, "color"=>"red", "fruit"=>"apple"}
300
+ # # {"_recordid"=>1, "color"=>"yellow", "fruit"=>"banana"}
301
+ # # {"_recordid"=>2, "color"=>"orange", "fruit"=>"orange"}>
302
+ #
303
+ def reorder_fields!(fields)
304
+ reserved, non_resreved = @field_list.reject {|f| fields.include? f }.partition {|f| f.start_with?("_") }
305
+ fs = reserved + fields + non_resreved
306
+ @field_list = @field_list.sort_by {|f| fs.index(f) }
307
+ end
308
+
309
+ # :call-seq:
310
+ # table.list_recordids -> [recordid1, recordid2, ...]
311
+ #
312
+ # returns the list of recordids as an array of integers.
313
+ #
314
+ # t = Tb.new %w[fruit color],
315
+ # %w[apple red],
316
+ # %w[banana yellow],
317
+ # %w[orange orange]
318
+ # pp t
319
+ # #=> #<Tb
320
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
321
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
322
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
323
+ # p t.list_recordids #=> [0, 1, 2]
324
+ #
325
+ def list_recordids
326
+ @tbl["_recordid"].compact
327
+ end
328
+
329
+ # :call-seq:
330
+ # table.size
331
+ #
332
+ # returns the number of records.
333
+ #
334
+ # t = Tb.new %w[fruit],
335
+ # %w[apple],
336
+ # %w[banana],
337
+ # %w[orange]
338
+ # pp t
339
+ # #=> #<Tb
340
+ # # {"_recordid"=>0, "fruit"=>"apple"}
341
+ # # {"_recordid"=>1, "fruit"=>"banana"}
342
+ # # {"_recordid"=>2, "fruit"=>"orange"}>
343
+ # p t.size
344
+ # #=> 3
345
+ #
346
+ def size
347
+ @recordid2index.size
348
+ end
349
+
350
+ # :call-seq:
351
+ # table.allocate_recordid -> fresh_recordid
352
+ # table.allocate_recordid(recordid) -> recordid
353
+ #
354
+ # inserts a record and returns its identifier.
355
+ # All fields of the record are initialized to nil.
356
+ #
357
+ # t = Tb.new %w[fruit color],
358
+ # %w[apple red],
359
+ # %w[banana yellow],
360
+ # %w[orange orange]
361
+ # pp t
362
+ # #=> #<Tb
363
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
364
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
365
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
366
+ # p t.allocate_recoridd #=> 3
367
+ # pp t
368
+ # #=> #<Tb
369
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
370
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
371
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}
372
+ # # {"_recordid"=>3}>
373
+ #
374
+ # If the optional recordid is specified and the recordid is not used in the
375
+ # table, a record is allocated with the recordid.
376
+ # If the specified recordid is already used, ArgumentError is raised.
377
+ #
378
+ # t = Tb.new %w[fruit color],
379
+ # %w[apple red],
380
+ # %w[banana yellow],
381
+ # %w[orange orange]
382
+ # pp t
383
+ # #=> #<Tb
384
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
385
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
386
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
387
+ # t.allocate_recordid(100)
388
+ # pp t
389
+ # #=> #<Tb
390
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
391
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
392
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}
393
+ # # {"_recordid"=>100}>
394
+ #
395
+ def allocate_recordid(recordid=nil)
396
+ if recordid.nil?
397
+ recordid = @next_recordid
398
+ @next_recordid += 1
399
+ else
400
+ recordid = check_recordid_type(recordid)
401
+ if @recordid2index.include? recordid
402
+ raise ArgumentError, "recordid already used: #{recordid.inspect}"
403
+ end
404
+ @next_recordid = recordid + 1 if @next_recordid <= recordid
405
+ end
406
+ if @free_index.empty?
407
+ index = @tbl["_recordid"].length
408
+ else
409
+ index = @free_index.pop
410
+ end
411
+ @recordid2index[recordid] = index
412
+ @tbl["_recordid"][index] = recordid
413
+ recordid
414
+ end
415
+
416
+ # :call-seq:
417
+ # table.allocate_record(recordid=nil)
418
+ #
419
+ # allocates a record.
420
+ #
421
+ # If the optional argument, _recordid_, is specified,
422
+ # the allocated record will have the recordid.
423
+ # If _recordid_ is already used, ArgumentError is raised.
424
+ def allocate_record(recordid=nil)
425
+ Tb::Record.new(self, allocate_recordid(recordid))
426
+ end
427
+
428
+ # :call-seq:
429
+ # table.set_cell(recordid, field, value) -> value
430
+ #
431
+ # sets the value of the cell identified by _recordid_ and _field_.
432
+ #
433
+ # t = Tb.new %w[fruit color],
434
+ # %w[apple red],
435
+ # %w[banana yellow],
436
+ # %w[orange orange]
437
+ # pp t
438
+ # #=> #<Tb
439
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
440
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
441
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
442
+ # t.set_cell(1, "color", "green")
443
+ # pp t
444
+ # #=> #<Tb
445
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
446
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"green"}
447
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
448
+ def set_cell(recordid, field, value)
449
+ recordid = check_recordid(recordid)
450
+ field = check_field(field)
451
+ raise ArgumentError, "can not set for reserved field: #{field.inspect}" if field.start_with?("_")
452
+ ary = @tbl[field]
453
+ ary[@recordid2index[recordid]] = value
454
+ end
455
+
456
+ # :call-seq:
457
+ # table.get_cell(recordid, field) -> value
458
+ #
459
+ # returns the value of the cell identified by _recordid_ and _field_.
460
+ #
461
+ # t = Tb.new %w[fruit color],
462
+ # %w[apple red],
463
+ # %w[banana yellow],
464
+ # %w[orange orange]
465
+ # pp t
466
+ # #=> #<Tb
467
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
468
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
469
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
470
+ # p t.get_cell(1, "fruit") #=> "banana"
471
+ #
472
+ def get_cell(recordid, field)
473
+ recordid = check_recordid(recordid)
474
+ field = check_field(field)
475
+ ary = @tbl[field]
476
+ ary[@recordid2index[recordid]]
477
+ end
478
+
479
+ # :call-seq:
480
+ # table.delete_cell(recordid, field) -> oldvalue
481
+ #
482
+ # sets nil to the cell identified by _recordid_ and _field_.
483
+ #
484
+ # This method returns the old value.
485
+ #
486
+ # t = Tb.new %w[fruit color],
487
+ # %w[apple red],
488
+ # %w[banana yellow],
489
+ # %w[orange orange]
490
+ # pp t
491
+ # #=> #<Tb
492
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
493
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
494
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
495
+ # p t.delete_cell(1, "color") #=> "yellow"
496
+ # pp t
497
+ # #=> #<Tb
498
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
499
+ # # {"_recordid"=>1, "fruit"=>"banana"}
500
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
501
+ # p t.get_cell(1, "color") #=> nil
502
+ #
503
+ def delete_cell(recordid, field)
504
+ recordid = check_recordid(recordid)
505
+ field = check_field(field)
506
+ raise ArgumentError, "can not delete reserved field: #{field.inspect}" if field.start_with?("_")
507
+ ary = @tbl[field]
508
+ index = @recordid2index[recordid]
509
+ old = ary[index]
510
+ ary[index] = nil
511
+ old
512
+ end
513
+
514
+ # :call-seq:
515
+ # table.delete_recordid(recordid) -> nil
516
+ #
517
+ # deletes a record identified by _recordid_.
518
+ #
519
+ # This method returns nil.
520
+ #
521
+ # t = Tb.new %w[fruit color],
522
+ # %w[apple red],
523
+ # %w[banana yellow],
524
+ # %w[orange orange]
525
+ # pp t
526
+ # #=> #<Tb
527
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
528
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
529
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
530
+ # p t.delete_recordid(1)
531
+ # #=> nil
532
+ # pp t
533
+ # #=> #<Tb
534
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
535
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
536
+ #
537
+ def delete_recordid(recordid)
538
+ recordid = check_recordid(recordid)
539
+ index = @recordid2index.delete(recordid)
540
+ @tbl.each {|f, ary|
541
+ ary[index] = nil
542
+ }
543
+ @free_index.push index
544
+ nil
545
+ end
546
+
547
+ # :call-seq:
548
+ # table.insert({field1=>value1, ...})
549
+ #
550
+ # inserts a record.
551
+ # The record is represented as a hash which keys are field names.
552
+ #
553
+ # This method returned the recordid of the inserted record.
554
+ #
555
+ # t = Tb.new %w[fruit color],
556
+ # %w[apple red],
557
+ # %w[banana yellow],
558
+ # %w[orange orange]
559
+ # pp t
560
+ # #=> #<Tb
561
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
562
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
563
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
564
+ # recordid = t.insert({"fruit"=>"grape", "color"=>"purple"})
565
+ # p recordid
566
+ # #=> 3
567
+ # pp t
568
+ # #=> #<Tb
569
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
570
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
571
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}
572
+ # # {"_recordid"=>3, "fruit"=>"grape", "color"=>"purple"}>
573
+ #
574
+ def insert(record)
575
+ recordid = allocate_recordid
576
+ update_record(recordid, record)
577
+ recordid
578
+ end
579
+
580
+ # call-seq
581
+ # table.insert_values(fields, values1, values2, ...) -> [recordid1, recordid2, ...]
582
+ #
583
+ # inserts records.
584
+ # The records are represented by fields and values separately.
585
+ # The first argument specifies the field names as an array.
586
+ # The second argument specifies the first record values as an array.
587
+ # The third argument specifies the second record values and so on.
588
+ # The third and subsequent arguments are optional.
589
+ #
590
+ # This method return an array of recordids.
591
+ #
592
+ # t = Tb.new %w[fruit color],
593
+ # %w[apple red],
594
+ # %w[banana yellow],
595
+ # %w[orange orange]
596
+ # pp t
597
+ # #=> #<Tb
598
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
599
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
600
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
601
+ # p t.insert_values(["fruit", "color"], ["grape", "purple"], ["cherry", "red"])
602
+ # #=> [3, 4]
603
+ # pp t
604
+ # #=> #<Tb
605
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
606
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
607
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}
608
+ # # {"_recordid"=>3, "fruit"=>"grape", "color"=>"purple"}
609
+ # # {"_recordid"=>4, "fruit"=>"cherry", "color"=>"red"}>
610
+ #
611
+ def insert_values(fields, *values_list)
612
+ recordids = []
613
+ values_list.each {|values|
614
+ if values.length != fields.length
615
+ raise ArgumentError, "#{fields.length} fields expected but #{values.length} values given"
616
+ end
617
+ h = {}
618
+ fields.each_with_index {|f, i|
619
+ v = values[i]
620
+ h[f] = v
621
+ }
622
+ recordids << insert(h)
623
+ }
624
+ recordids
625
+ end
626
+
627
+ # :call-seq:
628
+ # table1.concat(table2, table3, ...) -> table1
629
+ #
630
+ # concatenates argument tables destructively into _table1_.
631
+ # The reserved field (_recordid) in the argument tables is ignored.
632
+ #
633
+ # This method returns _table1_.
634
+ #
635
+ # t1 = Tb.new %w[fruit color],
636
+ # %w[apple red]
637
+ # t2 = Tb.new %w[fruit color],
638
+ # %w[banana yellow],
639
+ # %w[orange orange]
640
+ # pp t1
641
+ # #=> #<Tb {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}>
642
+ # pp t2
643
+ # #=> #<Tb
644
+ # {"_recordid"=>0, "fruit"=>"banana", "color"=>"yellow"}
645
+ # {"_recordid"=>1, "fruit"=>"orange", "color"=>"orange"}>
646
+ # t1.concat(t2)
647
+ # pp t1
648
+ # #=> #<Tb
649
+ # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
650
+ # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
651
+ # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
652
+ #
653
+ def concat(*tables)
654
+ tables.each {|t|
655
+ t.each_record {|record|
656
+ record = record.to_h
657
+ record.delete "_recordid"
658
+ self.insert record
659
+ }
660
+ }
661
+ self
662
+ end
663
+
664
+ # :call-seq:
665
+ # table.update_record(recordid, {field1=>value1, ...}) -> nil
666
+ #
667
+ # updates the record specified by _recordid_.
668
+ #
669
+ # t = Tb.new %w[fruit color],
670
+ # %w[apple red],
671
+ # %w[banana yellow],
672
+ # %w[orange orange]
673
+ # pp t
674
+ # #=> #<Tb
675
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
676
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
677
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
678
+ # p t.update_record(1, {"color"=>"green"})
679
+ # #=> nil
680
+ # pp t
681
+ # #=> #<Tb
682
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
683
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"green"}
684
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
685
+ #
686
+ def update_record(recordid, record)
687
+ recordid = check_recordid(recordid)
688
+ record.each {|f, v|
689
+ f = check_field(f)
690
+ set_cell(recordid, f, v)
691
+ }
692
+ nil
693
+ end
694
+
695
+ # :call-seq:
696
+ # table.get_values(recordid, field1, field2, ...) -> [value1, value2, ...]
697
+ #
698
+ # extracts specified fields of the specified record.
699
+ #
700
+ # t = Tb.new %w[fruit color],
701
+ # %w[apple red],
702
+ # %w[banana yellow],
703
+ # %w[orange orange]
704
+ # pp t
705
+ # #=> #<Tb
706
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
707
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
708
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
709
+ # p t.get_values(1, "fruit", "color")
710
+ # #=> ["banana", "yellow"]
711
+ # p t.get_values(0, "fruit")
712
+ # #=> ["apple"]
713
+ #
714
+ def get_values(recordid, *fields)
715
+ recordid = check_recordid(recordid)
716
+ fields.map {|f|
717
+ f = check_field(f)
718
+ get_cell(recordid, f)
719
+ }
720
+ end
721
+
722
+ # :call-seq:
723
+ # table.get_record(recordid) -> record
724
+ #
725
+ # get the record specified by _recordid_ as a hash.
726
+ #
727
+ # t = Tb.new %w[fruit color],
728
+ # %w[apple red],
729
+ # %w[banana yellow],
730
+ # %w[orange orange]
731
+ # pp t
732
+ # #=> #<Tb
733
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
734
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
735
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
736
+ # p t.get_record(1)
737
+ # #=> #<Tb::Record: "_recordid"=>1, "fruit"=>"banana", "color"=>"yellow">
738
+ #
739
+ def get_record(recordid)
740
+ recordid = check_recordid(recordid)
741
+ Tb::Record.new(self, recordid)
742
+ end
743
+
744
+ # :call-seq:
745
+ # table.each_field {|field| ... }
746
+ #
747
+ # iterates over the non-reserved field names of the table.
748
+ #
749
+ # t = Tb.new %w[fruit color],
750
+ # %w[apple red],
751
+ # %w[banana yellow],
752
+ # %w[orange orange]
753
+ # pp t
754
+ # #=> #<Tb
755
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
756
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
757
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
758
+ # t.each_field {|f| p f }
759
+ # #=> "fruit"
760
+ # # "color"
761
+ #
762
+ def each_field
763
+ @field_list.each {|f|
764
+ next if f.start_with?("_")
765
+ yield f
766
+ }
767
+ nil
768
+ end
769
+
770
+ # :call-seq:
771
+ # table.each_field_with_reserved {|field| ... }
772
+ #
773
+ # iterates over the reserved and non-reserved field names of the table.
774
+ #
775
+ # t = Tb.new %w[fruit color],
776
+ # %w[apple red],
777
+ # %w[banana yellow],
778
+ # %w[orange orange]
779
+ # pp t
780
+ # #=> #<Tb
781
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
782
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
783
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
784
+ # t.each_field {|f| p f }
785
+ # #=> "_recordid"
786
+ # # "fruit"
787
+ # # "color"
788
+ #
789
+ def each_field_with_reserved
790
+ @field_list.each {|f| yield f }
791
+ nil
792
+ end
793
+
794
+ # :call-seq:
795
+ # table.each_recordid {|recordid| ... }
796
+ #
797
+ # iterates over all records and yield the recordids of them.
798
+ #
799
+ # This method returns nil.
800
+ #
801
+ # t = Tb.new %w[fruit color],
802
+ # %w[apple red],
803
+ # %w[banana yellow],
804
+ # %w[orange orange]
805
+ # pp t
806
+ # #=> #<Tb
807
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
808
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
809
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
810
+ # t.each_recordid {|recordid| p recordid }
811
+ # #=> 0
812
+ # # 1
813
+ # # 2
814
+ #
815
+ def each_recordid
816
+ @tbl["_recordid"].each {|recordid|
817
+ next if recordid.nil?
818
+ yield recordid
819
+ }
820
+ nil
821
+ end
822
+
823
+ # :call-seq:
824
+ # table.to_a -> [record1, ...]
825
+ #
826
+ # returns an array containing all records as Tb::Record objects.
827
+ #
828
+ # t = Tb.new %w[fruit color],
829
+ # %w[apple red],
830
+ # %w[banana yellow],
831
+ # %w[orange orange]
832
+ # pp t
833
+ # #=> #<Tb
834
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
835
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
836
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
837
+ # pp t.to_a
838
+ # #=> [#<Tb::Record: "fruit"=>"apple", "color"=>"red">,
839
+ # # #<Tb::Record: "fruit"=>"banana", "color"=>"yellow">,
840
+ # # #<Tb::Record: "fruit"=>"orange", "color"=>"orange">]
841
+ #
842
+ def to_a
843
+ ary = []
844
+ each_recordid {|recordid|
845
+ ary << get_record(recordid)
846
+ }
847
+ ary
848
+ end
849
+
850
+ # :call-seq:
851
+ # table.each {|record| ... }
852
+ # table.each_record {|record| ... }
853
+ #
854
+ # iterates over all records and yields them as Tb::Record object.
855
+ #
856
+ # This method returns nil.
857
+ #
858
+ # t = Tb.new %w[fruit color],
859
+ # %w[apple red],
860
+ # %w[banana yellow],
861
+ # %w[orange orange]
862
+ # pp t
863
+ # #=> #<Tb
864
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
865
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
866
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
867
+ # t.each_record {|record| p record }
868
+ # #=> #<Tb::Record: "fruit"=>"apple", "color"=>"red">
869
+ # # #<Tb::Record: "fruit"=>"banana", "color"=>"yellow">
870
+ # # #<Tb::Record: "fruit"=>"orange", "color"=>"orange">
871
+ #
872
+ def each_record
873
+ each_recordid {|recordid|
874
+ yield get_record(recordid)
875
+ }
876
+ nil
877
+ end
878
+ alias each each_record
879
+
880
+ # :call-seq:
881
+ # table.each_record_values(field1, ...) {|value1, ...| ... }
882
+ def each_record_values(*fields)
883
+ each_recordid {|recordid|
884
+ vs = get_values(recordid, *fields)
885
+ yield vs
886
+ }
887
+ end
888
+
889
+ # :call-seq:
890
+ # table.filter {|record| ... }
891
+ def filter
892
+ t = Tb.new list_fields
893
+ each_record {|record|
894
+ if yield(record)
895
+ t.insert record
896
+ end
897
+ }
898
+ t
899
+ end
900
+
901
+ # :call-seq:
902
+ # table1.natjoin2(table2)
903
+ def natjoin2(table2)
904
+ table1 = self
905
+ fields1 = table1.list_fields
906
+ fields2 = table2.list_fields
907
+ common_fields = fields1 & fields2
908
+ total_fields = fields1 | fields2
909
+ unique_fields2 = fields2 - common_fields
910
+ h = {}
911
+ table2.each {|rec2|
912
+ k = rec2.values_at(*common_fields)
913
+ (h[k] ||= []) << rec2
914
+ }
915
+ result = Tb.new(fields1 | fields2)
916
+ table1.each {|rec1|
917
+ k = rec1.values_at(*common_fields)
918
+ rec2_list = h[k]
919
+ next if !rec2_list
920
+ values = rec1.values_at(*fields1)
921
+ rec2_list.each {|rec2|
922
+ result.insert_values total_fields, values + rec2.values_at(*unique_fields2)
923
+ }
924
+ }
925
+ result
926
+ end
927
+
928
+ # :call-seq:
929
+ # table1.natjoin2_outer(table2, missing=nil, retain_left=true, retain_right=true)
930
+ def natjoin2_outer(table2, missing=nil, retain_left=true, retain_right=true)
931
+ table1 = self
932
+ fields1 = table1.list_fields
933
+ fields2 = table2.list_fields
934
+ common_fields = fields1 & fields2
935
+ total_fields = fields1 | fields2
936
+ unique_fields2 = fields2 - common_fields
937
+ fields2_extended = total_fields.map {|f| fields2.include?(f) ? f : nil }
938
+ h = {}
939
+ table2.each {|rec2|
940
+ k = rec2.values_at(*common_fields)
941
+ (h[k] ||= []) << rec2
942
+ }
943
+ result = Tb.new(total_fields)
944
+ ids2 = {}
945
+ table1.each {|rec1|
946
+ k = rec1.values_at(*common_fields)
947
+ rec2_list = h[k]
948
+ values = rec1.values_at(*fields1)
949
+ if !rec2_list || rec2_list.empty?
950
+ if retain_left
951
+ result.insert_values total_fields, values + unique_fields2.map { missing }
952
+ end
953
+ else
954
+ rec2_list.each {|rec2|
955
+ ids2[rec2['_recordid']] = true
956
+ result.insert_values total_fields, values + rec2.values_at(*unique_fields2)
957
+ }
958
+ end
959
+ }
960
+ if retain_right
961
+ table2.each {|rec2|
962
+ if !ids2[rec2['_recordid']]
963
+ result.insert_values total_fields, fields2_extended.map {|f| f ? rec2[f] : missing }
964
+ end
965
+ }
966
+ end
967
+ result
968
+ end
969
+
970
+ # :call-seq:
971
+ # table.fmap!(field) {|record, value| new_value }
972
+ def fmap!(field)
973
+ each_recordid {|recordid|
974
+ value = yield get_record(recordid), get_cell(recordid, field)
975
+ set_cell(recordid, field, value)
976
+ }
977
+ end
978
+
979
+ # :call-seq:
980
+ # table.delete_field(field1, ...) -> nil
981
+ #
982
+ # deletes zero or more fields destructively.
983
+ #
984
+ # This method returns nil.
985
+ def delete_field(*fields)
986
+ fields.each {|f|
987
+ f = check_field(f)
988
+ raise ArgumentError, "can not delete reserved field: #{f.inspect}" if f.start_with?("_")
989
+ @tbl.delete(f)
990
+ @field_list.delete(f)
991
+ }
992
+ nil
993
+ end
994
+
995
+ # :call-seq:
996
+ # table.rename_field({old_field1=>new_field1, ...})
997
+ #
998
+ # creates a new table which field names are renamed.
999
+ #
1000
+ # t = Tb.new %w[fruit color],
1001
+ # %w[apple red],
1002
+ # %w[banana yellow],
1003
+ # %w[orange orange]
1004
+ # pp t
1005
+ # #=> #<Tb
1006
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
1007
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
1008
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
1009
+ # pp t.rename_field("fruit"=>"food")
1010
+ # #=> #<Tb
1011
+ # # {"_recordid"=>0, "food"=>"apple", "color"=>"red"}
1012
+ # # {"_recordid"=>1, "food"=>"banana", "color"=>"yellow"}
1013
+ # # {"_recordid"=>2, "food"=>"orange", "color"=>"orange"}>
1014
+ #
1015
+ def rename_field(rename_hash)
1016
+ rh = {}
1017
+ rename_hash.each {|of, nf|
1018
+ of = check_field(of)
1019
+ nf = check_field_type(nf)
1020
+ rh[of] = nf
1021
+ }
1022
+ result = Tb.new
1023
+ field_list = self.list_fields
1024
+ field_list.each {|of|
1025
+ nf = rh.fetch(of, of)
1026
+ result.define_field(nf)
1027
+ }
1028
+ each_recordid {|recordid|
1029
+ values = get_values(recordid, *field_list)
1030
+ result.allocate_recordid(recordid)
1031
+ field_list.each_with_index {|of, i|
1032
+ nf = rh.fetch(of, of)
1033
+ result.set_cell(recordid, nf, values[i])
1034
+ }
1035
+ }
1036
+ result
1037
+ end
1038
+
1039
+ # :call-seq:
1040
+ # table.reorder_records_by {|rec| ... }
1041
+ #
1042
+ # creates a new table object which has same records as _table_ but
1043
+ # the order of the records are sorted.
1044
+ #
1045
+ # The sort order is defined as similar manner to Enumerable#sort_by.
1046
+ #
1047
+ # t = Tb.new %w[fruit color],
1048
+ # %w[apple red],
1049
+ # %w[banana yellow],
1050
+ # %w[orange orange]
1051
+ # pp t
1052
+ # #=> #<Tb
1053
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
1054
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}
1055
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}>
1056
+ #
1057
+ # pp t.reorder_records_by {|rec| rec["color"] }
1058
+ # #=> #<Tb
1059
+ # # {"_recordid"=>2, "fruit"=>"orange", "color"=>"orange"}
1060
+ # # {"_recordid"=>0, "fruit"=>"apple", "color"=>"red"}
1061
+ # # {"_recordid"=>1, "fruit"=>"banana", "color"=>"yellow"}>
1062
+ #
1063
+ def reorder_records_by(&b)
1064
+ result = Tb.new self.list_fields
1065
+ self.sort_by(&b).each {|rec|
1066
+ recordid = result.allocate_recordid(rec["_recordid"])
1067
+ result.update_record(recordid, rec)
1068
+ }
1069
+ result
1070
+ end
1071
+ end