tb 0.9 → 1.0

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.
Files changed (83) hide show
  1. checksums.yaml +4 -4
  2. data/README +13 -11
  3. data/lib/tb.rb +14 -6
  4. data/lib/tb/catreader.rb +2 -2
  5. data/lib/tb/cmd_consecutive.rb +6 -2
  6. data/lib/tb/cmd_crop.rb +22 -3
  7. data/lib/tb/cmd_cross.rb +24 -0
  8. data/lib/tb/cmd_cut.rb +20 -10
  9. data/lib/tb/cmd_git.rb +20 -7
  10. data/lib/tb/cmd_group.rb +32 -0
  11. data/lib/tb/cmd_gsub.rb +21 -0
  12. data/lib/tb/cmd_join.rb +28 -0
  13. data/lib/tb/cmd_ls.rb +9 -0
  14. data/lib/tb/cmd_melt.rb +15 -0
  15. data/lib/tb/cmd_mheader.rb +15 -0
  16. data/lib/tb/cmd_nest.rb +27 -6
  17. data/lib/tb/cmd_newfield.rb +19 -2
  18. data/lib/tb/cmd_rename.rb +20 -0
  19. data/lib/tb/{cmd_grep.rb → cmd_search.rb} +37 -23
  20. data/lib/tb/cmd_shape.rb +69 -25
  21. data/lib/tb/cmd_sort.rb +20 -0
  22. data/lib/tb/cmd_tar.rb +38 -0
  23. data/lib/tb/cmd_to_json.rb +2 -2
  24. data/lib/tb/cmd_to_ltsv.rb +3 -3
  25. data/lib/tb/cmd_to_pnm.rb +3 -3
  26. data/lib/tb/cmd_to_tsv.rb +3 -3
  27. data/lib/tb/cmd_to_yaml.rb +3 -3
  28. data/lib/tb/cmd_unmelt.rb +15 -0
  29. data/lib/tb/cmd_unnest.rb +31 -7
  30. data/lib/tb/cmdmain.rb +2 -0
  31. data/lib/tb/cmdtop.rb +1 -1
  32. data/lib/tb/cmdutil.rb +9 -62
  33. data/lib/tb/csv.rb +21 -79
  34. data/lib/tb/enumerable.rb +42 -68
  35. data/lib/tb/enumerator.rb +15 -7
  36. data/lib/tb/{fieldset.rb → hashreader.rb} +37 -56
  37. data/lib/tb/hashwriter.rb +54 -0
  38. data/lib/tb/headerreader.rb +108 -0
  39. data/lib/tb/headerwriter.rb +116 -0
  40. data/lib/tb/json.rb +17 -15
  41. data/lib/tb/ltsv.rb +35 -96
  42. data/lib/tb/ndjson.rb +63 -0
  43. data/lib/tb/numericreader.rb +66 -0
  44. data/lib/tb/numericwriter.rb +61 -0
  45. data/lib/tb/pnm.rb +206 -200
  46. data/lib/tb/ropen.rb +54 -59
  47. data/lib/tb/tsv.rb +39 -71
  48. data/sample/excel2csv +24 -25
  49. data/sample/poi-xls2csv.rb +13 -14
  50. data/tb.gemspec +154 -0
  51. data/test/test_cmd_cat.rb +28 -6
  52. data/test/test_cmd_consecutive.rb +8 -3
  53. data/test/test_cmd_cut.rb +14 -4
  54. data/test/test_cmd_git_log.rb +50 -50
  55. data/test/test_cmd_grep.rb +6 -6
  56. data/test/test_cmd_gsub.rb +7 -2
  57. data/test/test_cmd_ls.rb +70 -62
  58. data/test/test_cmd_shape.rb +43 -6
  59. data/test/test_cmd_svn_log.rb +26 -27
  60. data/test/test_cmd_to_csv.rb +10 -5
  61. data/test/test_cmd_to_json.rb +16 -0
  62. data/test/test_cmd_to_ltsv.rb +2 -2
  63. data/test/test_cmd_to_pp.rb +7 -2
  64. data/test/test_csv.rb +74 -62
  65. data/test/test_ex_enumerable.rb +0 -1
  66. data/test/test_fileenumerator.rb +3 -3
  67. data/test/test_headercsv.rb +43 -0
  68. data/test/test_json.rb +2 -2
  69. data/test/test_ltsv.rb +22 -17
  70. data/test/test_ndjson.rb +62 -0
  71. data/test/test_numericcsv.rb +36 -0
  72. data/test/test_pnm.rb +69 -70
  73. data/test/test_reader.rb +27 -124
  74. data/test/test_tbenum.rb +18 -18
  75. data/test/test_tsv.rb +21 -32
  76. data/test/util_tbtest.rb +12 -0
  77. metadata +41 -19
  78. data/lib/tb/basic.rb +0 -1070
  79. data/lib/tb/reader.rb +0 -106
  80. data/lib/tb/record.rb +0 -158
  81. data/test/test_basic.rb +0 -403
  82. data/test/test_fieldset.rb +0 -42
  83. data/test/test_record.rb +0 -61
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2011-2012 Tanaka Akira <akr@fsij.org>
1
+ # Copyright (C) 2011-2014 Tanaka Akira <akr@fsij.org>
2
2
  #
3
3
  # Redistribution and use in source and binary forms, with or without
4
4
  # modification, are permitted provided that the following conditions
@@ -40,9 +40,9 @@ def (Tb::Cmd).main_to_pnm(argv)
40
40
  op_to_pnm.parse!(argv)
41
41
  exit_if_help('to-pnm')
42
42
  argv = ['-'] if argv.empty?
43
- tbl = Tb::CatReader.open(argv, Tb::Cmd.opt_N).to_tb
43
+ reader = Tb::CatReader.open(argv, Tb::Cmd.opt_N)
44
44
  with_output {|out|
45
- tbl.generate_pnm(out)
45
+ reader.write_to_pnm(out)
46
46
  }
47
47
  end
48
48
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2011-2012 Tanaka Akira <akr@fsij.org>
1
+ # Copyright (C) 2011-2014 Tanaka Akira <akr@fsij.org>
2
2
  #
3
3
  # Redistribution and use in source and binary forms, with or without
4
4
  # modification, are permitted provided that the following conditions
@@ -40,9 +40,9 @@ def (Tb::Cmd).main_to_tsv(argv)
40
40
  op_to_tsv.parse!(argv)
41
41
  exit_if_help('to-tsv')
42
42
  argv = ['-'] if argv.empty?
43
- tbl = Tb::CatReader.open(argv, Tb::Cmd.opt_N).to_tb
43
+ reader = Tb::CatReader.open(argv, Tb::Cmd.opt_N)
44
44
  with_output {|out|
45
- tbl_generate_tsv(tbl, out)
45
+ reader.write_to_tsv(out, !Tb::Cmd.opt_N)
46
46
  }
47
47
  end
48
48
 
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2011-2012 Tanaka Akira <akr@fsij.org>
1
+ # Copyright (C) 2011-2014 Tanaka Akira <akr@fsij.org>
2
2
  #
3
3
  # Redistribution and use in source and binary forms, with or without
4
4
  # modification, are permitted provided that the following conditions
@@ -41,8 +41,8 @@ def (Tb::Cmd).main_to_yaml(argv)
41
41
  op_to_yaml.parse!(argv)
42
42
  exit_if_help('to-yaml')
43
43
  argv = ['-'] if argv.empty?
44
- tbl = Tb::CatReader.open(argv, Tb::Cmd.opt_N).to_tb
45
- ary = tbl.map {|rec| rec.to_h }
44
+ reader = Tb::CatReader.open(argv, Tb::Cmd.opt_N)
45
+ ary = reader.to_a
46
46
  with_output {|out|
47
47
  YAML.dump(ary, out)
48
48
  out.puts
@@ -58,6 +58,21 @@ def (Tb::Cmd).op_unmelt
58
58
  op
59
59
  end
60
60
 
61
+ Tb::Cmd.def_vhelp('unmelt', <<'End')
62
+ Example:
63
+
64
+ % cat tst.csv
65
+ foo,variable,value
66
+ A,bar,1
67
+ A,baz,x
68
+ B,bar,2
69
+ B,baz,y
70
+ % tb unmelt tst.csv
71
+ foo,bar,baz
72
+ A,1,x
73
+ B,2,y
74
+ End
75
+
61
76
  def (Tb::Cmd).main_unmelt(argv)
62
77
  op_unmelt.parse!(argv)
63
78
  exit_if_help('unmelt')
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2011-2012 Tanaka Akira <akr@fsij.org>
1
+ # Copyright (C) 2011-2014 Tanaka Akira <akr@fsij.org>
2
2
  #
3
3
  # Redistribution and use in source and binary forms, with or without
4
4
  # modification, are permitted provided that the following conditions
@@ -41,6 +41,27 @@ def (Tb::Cmd).op_unnest
41
41
  op
42
42
  end
43
43
 
44
+ Tb::Cmd.def_vhelp('unnest', <<'End')
45
+ Example:
46
+
47
+ % cat tst.csv
48
+ author,item
49
+ A,"name,length
50
+ foo,3
51
+ bar,5
52
+ "
53
+ B,"name,length
54
+ baz,2
55
+ qux,8
56
+ "
57
+ % tb unnest item tst.csv
58
+ author,name,length
59
+ A,foo,3
60
+ A,bar,5
61
+ B,baz,2
62
+ B,qux,8
63
+ End
64
+
44
65
  def (Tb::Cmd).main_unnest(argv)
45
66
  op_unnest.parse!(argv)
46
67
  exit_if_help('unnest')
@@ -64,13 +85,16 @@ def (Tb::Cmd).main_unnest(argv)
64
85
  elsif v.nil?
65
86
  pairs2[f] = v
66
87
  else
67
- nested_tbl = Tb.parse_csv(v)
68
- nested_tbl.list_fields.each {|f2|
88
+ aa = CSV.parse(v)
89
+ reader = Tb::HeaderReader.new(lambda { aa.shift })
90
+ nested_tbl_rows = reader.to_a
91
+ nested_tbl_header = reader.get_named_header
92
+ nested_tbl_header.each {|f2|
69
93
  unless nested_fields.has_key? f2
70
94
  nested_fields[f2] = nested_fields.size
71
95
  end
72
96
  }
73
- pairs2[f] = nested_tbl
97
+ pairs2[f] = [nested_tbl_header, nested_tbl_rows]
74
98
  end
75
99
  }
76
100
  y2.yield pairs2
@@ -93,13 +117,13 @@ def (Tb::Cmd).main_unnest(argv)
93
117
  }.each {|pairs|
94
118
  pairs2 = {}
95
119
  pairs.each {|f, v| pairs2[f] = v if f != target_field }
96
- ntbl = pairs[target_field]
97
- if ntbl.nil? || ntbl.empty?
120
+ ntbl_header, ntbl_rows = pairs[target_field]
121
+ if ntbl_header.nil? || ntbl_rows.empty?
98
122
  if Tb::Cmd.opt_unnest_outer
99
123
  y.yield pairs2
100
124
  end
101
125
  else
102
- ntbl.each {|npairs|
126
+ ntbl_rows.each {|npairs|
103
127
  pairs3 = pairs2.dup
104
128
  npairs.each {|nf, nv|
105
129
  if Tb::Cmd.opt_unnest_prefix
@@ -45,5 +45,7 @@ def (Tb::Cmd).main(argv)
45
45
  rescue SystemExit
46
46
  $stderr.puts $!.message if $!.message != 'exit'
47
47
  raise
48
+ rescue Errno::EPIPE
49
+ exit false
48
50
  end
49
51
 
@@ -43,7 +43,7 @@ require 'tb/cmd_to_pnm'
43
43
  require 'tb/cmd_to_json'
44
44
  require 'tb/cmd_to_yaml'
45
45
  require 'tb/cmd_to_pp'
46
- require 'tb/cmd_grep'
46
+ require 'tb/cmd_search'
47
47
  require 'tb/cmd_gsub'
48
48
  require 'tb/cmd_sort'
49
49
  require 'tb/cmd_cut'
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2011-2013 Tanaka Akira <akr@fsij.org>
1
+ # Copyright (C) 2011-2014 Tanaka Akira <akr@fsij.org>
2
2
  #
3
3
  # Redistribution and use in source and binary forms, with or without
4
4
  # modification, are permitted provided that the following conditions
@@ -131,39 +131,11 @@ def split_field_list_argument(arg)
131
131
  end
132
132
 
133
133
  def split_csv_argument(arg)
134
- Tb.csv_stream_input(arg) {|ary| return ary }
135
- return []
134
+ return CSV.new(arg).shift || []
136
135
  end
137
136
 
138
137
  def tablereader_open(filename, &b)
139
- Tb.open_reader(filename, {:numeric=>Tb::Cmd.opt_N}, &b)
140
- end
141
-
142
- def tbl_generate_tsv(tbl, out)
143
- if Tb::Cmd.opt_N
144
- header = tbl.list_fields
145
- Tb.tsv_stream_output(out) {|gen|
146
- tbl.each {|rec|
147
- gen << rec.values_at(*header)
148
- }
149
- }
150
- else
151
- tbl.generate_tsv(out)
152
- end
153
- end
154
-
155
- def tbl_generate_ltsv(tbl, out)
156
- if Tb::Cmd.opt_N
157
- header = tbl.list_fields
158
- Tb.ltsv_stream_output(out) {|gen|
159
- tbl.each {|rec|
160
- assoc = header.map {|f| [f, rec[f]] }
161
- gen << assoc
162
- }
163
- }
164
- else
165
- tbl.generate_ltsv(out)
166
- end
138
+ Tb.open_reader(filename, Tb::Cmd.opt_N, &b)
167
139
  end
168
140
 
169
141
  def with_output(filename=Tb::Cmd.opt_output)
@@ -191,37 +163,12 @@ def with_output(filename=Tb::Cmd.opt_output)
191
163
  end
192
164
 
193
165
  def output_tbenum(te)
194
- filename = Tb::Cmd.opt_output
195
- if /\A([a-z0-9]{2,}):/ =~ filename
196
- fmt = $1
197
- filename = $'
198
- else
199
- fmt = nil
200
- end
201
- if !fmt
202
- case filename
203
- when /\.csv\z/
204
- fmt = 'csv'
205
- when /\.ltsv\z/
206
- fmt = 'ltsv'
207
- when /\.json\z/
208
- fmt = 'json'
209
- end
210
- end
211
- if fmt
212
- case fmt
213
- when 'csv'
214
- write_proc = lambda {|out| te.write_to_csv(out, !Tb::Cmd.opt_N) }
215
- when 'ltsv'
216
- write_proc = lambda {|out| te.write_to_ltsv(out) }
217
- when 'json'
218
- write_proc = lambda {|out| te.write_to_json(out) }
219
- else
220
- err("unexpected format: #{fmt.inspect}")
221
- end
222
- end
223
- write_proc ||= lambda {|out| te.write_to_csv(out, !Tb::Cmd.opt_N) }
166
+ filename = Tb::Cmd.opt_output || '-'
167
+ numeric = Tb::Cmd.opt_N
168
+ filename, fmt = Tb.undecorate_filename(filename, numeric)
169
+ factory = Tb::FormatHash.fetch(fmt)[:writer]
224
170
  with_output(filename) {|out|
225
- write_proc.call(out)
171
+ writer = factory.new(out)
172
+ te.write_with(writer)
226
173
  }
227
174
  end
@@ -1,6 +1,6 @@
1
1
  # lib/tb/csv.rb - CSV related fetures for table library
2
2
  #
3
- # Copyright (C) 2010-2013 Tanaka Akira <akr@fsij.org>
3
+ # Copyright (C) 2010-2014 Tanaka Akira <akr@fsij.org>
4
4
  #
5
5
  # Redistribution and use in source and binary forms, with or without
6
6
  # modification, are permitted provided that the following conditions
@@ -30,95 +30,37 @@
30
30
 
31
31
  require 'csv'
32
32
 
33
- class Tb
34
- def Tb.load_csv(filename, *header_fields, &block)
35
- Tb.parse_csv(File.read(filename), *header_fields, &block)
33
+ module Tb
34
+ def Tb.csv_encode_row(ary)
35
+ ary.to_csv
36
36
  end
37
37
 
38
- def Tb.parse_csv(csv, *header_fields)
39
- aa = []
40
- csv_stream_input(csv) {|ary|
41
- aa << ary
42
- }
43
- aa = yield aa if block_given?
44
- if header_fields.empty?
45
- reader = Tb::Reader.new {|body| body.call(aa) }
46
- reader.to_tb
47
- else
48
- header = header_fields
49
- arys = aa
50
- t = Tb.new(header)
51
- arys.each {|ary|
52
- ary << nil while ary.length < header.length
53
- t.insert_values header, ary
54
- }
55
- t
38
+ class HeaderCSVReader < HeaderReader
39
+ def initialize(io)
40
+ aryreader = CSV.new(io)
41
+ super lambda { aryreader.shift }
56
42
  end
57
43
  end
58
44
 
59
- def Tb.csv_stream_input(csv, &b)
60
- csvreader = CSVReader.new(csv)
61
- csvreader.each(&b)
62
- nil
63
- end
64
-
65
- def Tb.csv_read_aa(csv)
66
- aa = []
67
- Tb.csv_stream_input(csv) {|ary| aa << ary }
68
- aa
69
- end
70
-
71
- class CSVReader
72
- def initialize(input)
73
- @csv = CSV.new(input)
74
- end
75
-
76
- def shift
77
- @csv.shift
78
- end
79
-
80
- def each
81
- while ary = self.shift
82
- yield ary
83
- end
84
- nil
45
+ class HeaderCSVWriter < HeaderWriter
46
+ # io is an object which has "<<" method.
47
+ def initialize(io)
48
+ super lambda {|ary| io << ary.to_csv}
85
49
  end
86
50
  end
87
51
 
88
- def Tb.csv_stream_output(out)
89
- require 'csv'
90
- gen = Object.new
91
- gen.instance_variable_set(:@out, out)
92
- def gen.<<(ary)
93
- @out << ary.to_csv
52
+ class NumericCSVReader < NumericReader
53
+ def initialize(io)
54
+ aryreader = CSV.new(io)
55
+ super lambda { aryreader.shift }
94
56
  end
95
- yield gen
96
- end
97
-
98
- def Tb.csv_encode_row(ary)
99
- require 'csv'
100
- ary.to_csv
101
57
  end
102
58
 
103
- # :call-seq:
104
- # generate_csv(out='', fields=nil) {|recordids| modified_recordids }
105
- # generate_csv(out='', fields=nil)
106
- #
107
- def generate_csv(out='', fields=nil, &block)
108
- if fields.nil?
109
- fields = list_fields
59
+ class NumericCSVWriter < NumericWriter
60
+ # io is an object which has "<<" method.
61
+ def initialize(io)
62
+ super lambda {|ary| io << ary.to_csv }
110
63
  end
111
- require 'csv'
112
- recordids = list_recordids
113
- if block_given?
114
- recordids = yield(recordids)
115
- end
116
- Tb.csv_stream_output(out) {|gen|
117
- gen << fields
118
- recordids.each {|recordid|
119
- gen << get_values(recordid, *fields)
120
- }
121
- }
122
- out
123
64
  end
65
+
124
66
  end
@@ -1,4 +1,4 @@
1
- # Copyright (C) 2012-2013 Tanaka Akira <akr@fsij.org>
1
+ # Copyright (C) 2012-2014 Tanaka Akira <akr@fsij.org>
2
2
  #
3
3
  # Redistribution and use in source and binary forms, with or without
4
4
  # modification, are permitted provided that the following conditions
@@ -93,7 +93,7 @@ module Tb::Enumerable
93
93
  # creates a new Tb::Enumerator object which have
94
94
  # new field named by _field_ with the value returned by the block.
95
95
  #
96
- # t1 = Tb.new %w[a b], [1, 2], [3, 4]
96
+ # t1 = Tb::Enumerator.from_header_and_values %w[a b], [1, 2], [3, 4]
97
97
  # p t1.newfield("x") {|row| row["a"] + row["b"] + 100 }.to_a
98
98
  # #=> [{"x"=>103, "a"=>1, "b"=>2},
99
99
  # # {"x"=>107, "a"=>3, "b"=>4}]
@@ -102,11 +102,11 @@ module Tb::Enumerable
102
102
  Tb::Enumerator.new {|y|
103
103
  self.with_header {|header|
104
104
  if header
105
- y.set_header(Tb::FieldSet.normalize([field, *header]))
105
+ y.set_header([field, *header])
106
106
  end
107
107
  }.each {|row|
108
108
  keys = row.keys
109
- keys = Tb::FieldSet.normalize([field, *keys])
109
+ keys = [field, *keys]
110
110
  vals = row.values
111
111
  vals = [yield(row), *vals]
112
112
  y << Hash[keys.zip(vals)]
@@ -176,80 +176,46 @@ module Tb::Enumerable
176
176
  natjoin2(tbl2, missing_value, retain_left, retain_right)
177
177
  end
178
178
 
179
- def to_tb
180
- tb = Tb.new
181
- self.each {|pairs|
182
- pairs.each {|k, v|
183
- unless tb.has_field? k
184
- tb.define_field(k)
185
- end
179
+ def write_with(writer)
180
+ header_proc = nil
181
+ if writer.header_required?
182
+ header_proc = lambda {|header|
183
+ writer.header_generator = lambda { header }
186
184
  }
187
- tb.insert pairs
185
+ end
186
+ body_proc = lambda {|pairs|
187
+ writer.put_hash pairs
188
188
  }
189
- tb
189
+ header_and_each(header_proc, &body_proc)
190
+ writer.finish
190
191
  end
191
192
 
192
193
  def write_to_csv(io, with_header=true)
193
- stream = nil
194
- header = []
195
- fgen = fnew = nil
196
- self.with_cumulative_header {|header0|
197
- if !with_header
198
- stream = true
199
- elsif header0
200
- stream = true
201
- io.puts Tb.csv_encode_row(header0)
202
- else
203
- stream = false
204
- fgen, fnew = Tb::FileEnumerator.gen_new
205
- end
206
- }.each {|pairs, header1|
207
- pairs = Hash[pairs] unless pairs.respond_to? :has_key?
208
- header = header1
209
- if stream
210
- fs = header.dup
211
- while !fs.empty? && !pairs.has_key?(fs.last)
212
- fs.pop
213
- end
214
- ary = fs.map {|f| pairs[f] }
215
- io.puts Tb.csv_encode_row(ary)
216
- else
217
- fgen.call Hash[pairs]
218
- end
219
- }
220
- if !stream
221
- if with_header
222
- io.puts Tb.csv_encode_row(header)
223
- end
224
- fnew.call.each {|pairs|
225
- fs = header.dup
226
- while !fs.empty? && !pairs.has_key?(fs.last)
227
- fs.pop
228
- end
229
- ary = fs.map {|f| pairs[f] }
230
- io.puts Tb.csv_encode_row(ary)
231
- }
194
+ if with_header
195
+ write_with(Tb::HeaderCSVWriter.new(io))
196
+ else
197
+ write_with(Tb::NumericCSVWriter.new(io))
232
198
  end
233
199
  end
234
200
 
235
- def write_to_ltsv(out)
236
- self.each {|pairs|
237
- out.print Tb.ltsv_assoc_join(pairs)
238
- out.print "\n"
239
- }
201
+ def write_to_tsv(io, with_header=true)
202
+ if with_header
203
+ write_with(Tb::HeaderTSVWriter.new(io))
204
+ else
205
+ write_with(Tb::NumericTSVWriter.new(io))
206
+ end
240
207
  end
241
208
 
242
- def write_to_json(out)
243
- require 'json'
244
- out.print "["
245
- sep = nil
246
- self.each {|pairs|
247
- out.print sep if sep
248
- out.print JSON.pretty_generate(Hash[pairs.to_a])
249
- sep = ",\n"
250
- }
251
- out.puts "]"
252
- nil
209
+ def write_to_ltsv(io)
210
+ write_with(Tb::LTSVWriter.new(io))
211
+ end
212
+
213
+ def write_to_json(io)
214
+ write_with(Tb::JSONWriter.new(io))
215
+ end
216
+
217
+ def write_to_pnm(io)
218
+ write_with(Tb::PNMWriter.new(io))
253
219
  end
254
220
 
255
221
  def extsort_by(opts={}, &cmpvalue_from)
@@ -270,3 +236,11 @@ module Tb::Enumerable
270
236
  }
271
237
  end
272
238
  end
239
+
240
+ module Tb::EnumerableWithEach
241
+ include Tb::Enumerable
242
+
243
+ def each(&b)
244
+ header_and_each(nil, &b)
245
+ end
246
+ end