tb 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,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