tb 0.1 → 0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (81) hide show
  1. data/README +156 -5
  2. data/bin/tb +2 -1110
  3. data/lib/tb.rb +4 -2
  4. data/lib/tb/catreader.rb +131 -0
  5. data/lib/tb/cmd_cat.rb +65 -0
  6. data/lib/tb/cmd_consecutive.rb +79 -0
  7. data/lib/tb/cmd_crop.rb +105 -0
  8. data/lib/tb/cmd_cross.rb +119 -0
  9. data/lib/tb/cmd_csv.rb +42 -0
  10. data/lib/tb/cmd_cut.rb +77 -0
  11. data/lib/tb/cmd_grep.rb +76 -0
  12. data/lib/tb/cmd_group.rb +82 -0
  13. data/lib/tb/cmd_gsub.rb +77 -0
  14. data/lib/tb/cmd_help.rb +98 -0
  15. data/lib/tb/cmd_join.rb +81 -0
  16. data/lib/tb/cmd_json.rb +60 -0
  17. data/lib/tb/cmd_ls.rb +273 -0
  18. data/lib/tb/cmd_mheader.rb +77 -0
  19. data/lib/tb/cmd_newfield.rb +59 -0
  20. data/lib/tb/cmd_pnm.rb +43 -0
  21. data/lib/tb/cmd_pp.rb +70 -0
  22. data/lib/tb/cmd_rename.rb +58 -0
  23. data/lib/tb/cmd_shape.rb +67 -0
  24. data/lib/tb/cmd_sort.rb +58 -0
  25. data/lib/tb/cmd_svn_log.rb +158 -0
  26. data/lib/tb/cmd_tsv.rb +43 -0
  27. data/lib/tb/cmd_yaml.rb +47 -0
  28. data/lib/tb/cmdmain.rb +45 -0
  29. data/lib/tb/cmdtop.rb +58 -0
  30. data/lib/tb/cmdutil.rb +327 -0
  31. data/lib/tb/csv.rb +30 -6
  32. data/lib/tb/fieldset.rb +39 -41
  33. data/lib/tb/pager.rb +132 -0
  34. data/lib/tb/pnm.rb +357 -0
  35. data/lib/tb/reader.rb +18 -128
  36. data/lib/tb/record.rb +3 -3
  37. data/lib/tb/ropen.rb +70 -0
  38. data/lib/tb/{pathfinder.rb → search.rb} +69 -34
  39. data/lib/tb/tsv.rb +29 -1
  40. data/sample/colors.ppm +0 -0
  41. data/sample/gradation.pgm +0 -0
  42. data/sample/langs.csv +46 -0
  43. data/sample/tbplot +293 -0
  44. data/test-all-cov.rb +65 -0
  45. data/test-all.rb +5 -0
  46. data/test/test_basic.rb +99 -2
  47. data/test/test_catreader.rb +27 -0
  48. data/test/test_cmd_cat.rb +118 -0
  49. data/test/test_cmd_consecutive.rb +90 -0
  50. data/test/test_cmd_crop.rb +101 -0
  51. data/test/test_cmd_cross.rb +113 -0
  52. data/test/test_cmd_csv.rb +129 -0
  53. data/test/test_cmd_cut.rb +100 -0
  54. data/test/test_cmd_grep.rb +89 -0
  55. data/test/test_cmd_group.rb +181 -0
  56. data/test/test_cmd_gsub.rb +103 -0
  57. data/test/test_cmd_help.rb +190 -0
  58. data/test/test_cmd_join.rb +197 -0
  59. data/test/test_cmd_json.rb +75 -0
  60. data/test/test_cmd_ls.rb +203 -0
  61. data/test/test_cmd_mheader.rb +86 -0
  62. data/test/test_cmd_newfield.rb +63 -0
  63. data/test/test_cmd_pnm.rb +35 -0
  64. data/test/test_cmd_pp.rb +62 -0
  65. data/test/test_cmd_rename.rb +91 -0
  66. data/test/test_cmd_shape.rb +50 -0
  67. data/test/test_cmd_sort.rb +105 -0
  68. data/test/test_cmd_tsv.rb +67 -0
  69. data/test/test_cmd_yaml.rb +55 -0
  70. data/test/test_cmdtty.rb +154 -0
  71. data/test/test_cmdutil.rb +43 -0
  72. data/test/test_csv.rb +10 -0
  73. data/test/test_fieldset.rb +42 -0
  74. data/test/test_pager.rb +142 -0
  75. data/test/test_pnm.rb +374 -0
  76. data/test/test_reader.rb +147 -0
  77. data/test/test_record.rb +49 -0
  78. data/test/test_search.rb +575 -0
  79. data/test/test_tsv.rb +7 -0
  80. metadata +108 -5
  81. data/lib/tb/qtsv.rb +0 -93
data/lib/tb.rb CHANGED
@@ -28,8 +28,10 @@ require 'tb/basic'
28
28
  require 'tb/record'
29
29
  require 'tb/csv'
30
30
  require 'tb/tsv'
31
- require 'tb/qtsv'
31
+ require 'tb/pnm'
32
32
  require 'tb/reader'
33
+ require 'tb/ropen'
34
+ require 'tb/catreader'
33
35
  require 'tb/fieldset'
34
- require 'tb/pathfinder'
36
+ require 'tb/search'
35
37
  require 'tb/enumerable'
@@ -0,0 +1,131 @@
1
+ # lib/tb/catreader.rb - Tb::CatReader class
2
+ #
3
+ # Copyright (C) 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
+ class Tb::CatReader
28
+ def self.open(filenames, numeric=false)
29
+ readers = []
30
+ filenames.each {|f|
31
+ readers << Tb::Reader.open(f, numeric ? {:numeric=>true} : {})
32
+ }
33
+ r = Tb::CatReader.new(readers, readers, numeric)
34
+ if block_given?
35
+ begin
36
+ yield r
37
+ ensure
38
+ r.close
39
+ end
40
+ else
41
+ r
42
+ end
43
+ end
44
+
45
+ def initialize(readers, also_close, numeric)
46
+ @readers = readers.dup
47
+ @also_close = also_close
48
+ @numeric = numeric
49
+ @fieldset = nil
50
+ end
51
+
52
+ def header
53
+ return @fieldset.header if @fieldset
54
+ if @numeric
55
+ @fieldset = Tb::FieldSet.new
56
+ else
57
+ h = {}
58
+ @readers.each {|r|
59
+ r.header.each {|f|
60
+ if !h[f]
61
+ h[f] = h.size
62
+ end
63
+ }
64
+ }
65
+ @fieldset = Tb::FieldSet.new(*h.keys.sort_by {|f| h[f] })
66
+ end
67
+ return @fieldset.header
68
+ end
69
+
70
+ def index_from_field_ex(f)
71
+ self.header
72
+ @fieldset.index_from_field_ex(f)
73
+ end
74
+
75
+ def index_from_field(f)
76
+ self.header
77
+ @fieldset.index_from_field(f)
78
+ end
79
+
80
+ def field_from_index_ex(i)
81
+ self.header
82
+ @fieldset.field_from_index_ex(i)
83
+ end
84
+
85
+ def field_from_index(i)
86
+ self.header
87
+ @fieldset.field_from_index(i)
88
+ end
89
+
90
+ def shift
91
+ self.header
92
+ while !@readers.empty?
93
+ r = @readers.first
94
+ ary = r.shift
95
+ if ary
96
+ h = r.header
97
+ ary2 = []
98
+ ary.each_with_index {|v,i|
99
+ f = h[i]
100
+ i2 = @fieldset.index_from_field_ex(f)
101
+ ary2[i2] = v
102
+ }
103
+ return ary2
104
+ else
105
+ @readers.shift
106
+ end
107
+ end
108
+ nil
109
+ end
110
+
111
+ def each
112
+ while ary = self.shift
113
+ yield ary
114
+ end
115
+ nil
116
+ end
117
+
118
+ def read_all
119
+ result = []
120
+ while ary = self.shift
121
+ result << ary
122
+ end
123
+ result
124
+ end
125
+
126
+ def close
127
+ if @also_close
128
+ @also_close.each {|x| x.close }
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,65 @@
1
+ # Copyright (C) 2011 Tanaka Akira <akr@fsij.org>
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # 1. Redistributions of source code must retain the above copyright notice, this
7
+ # list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # 3. The name of the author may not be used to endorse or promote products
12
+ # derived from this software without specific prior written permission.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
19
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
22
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
23
+ # OF SUCH DAMAGE.
24
+
25
+ Tb::Cmd.subcommands << 'cat'
26
+
27
+ def (Tb::Cmd).op_cat
28
+ op = OptionParser.new
29
+ op.banner = 'Usage: tb cat [OPTS] [TABLE ...]'
30
+ define_common_option(op, "hNo", "--no-pager")
31
+ op
32
+ end
33
+
34
+ Tb::Cmd.def_vhelp('cat', <<'End')
35
+ Example:
36
+
37
+ % cat tst1.csv
38
+ a,b,c
39
+ 0,1,2
40
+ 4,5,6
41
+ % cat tst2.csv
42
+ a,b,d
43
+ U,V,W
44
+ X,Y,Z
45
+ % tb cat tst1.csv tst2.csv
46
+ a,b,c,d
47
+ 0,1,2
48
+ 4,5,6
49
+ U,V,,W
50
+ X,Y,,Z
51
+ End
52
+
53
+ def (Tb::Cmd).main_cat(argv)
54
+ op_cat.parse!(argv)
55
+ exit_if_help('cat')
56
+ argv = ['-'] if argv.empty?
57
+ creader = Tb::CatReader.open(argv, Tb::Cmd.opt_N)
58
+ header = creader.header
59
+ with_table_stream_output {|gen|
60
+ gen << header if !Tb::Cmd.opt_N
61
+ creader.each {|ary|
62
+ gen << ary
63
+ }
64
+ }
65
+ end
@@ -0,0 +1,79 @@
1
+ # Copyright (C) 2011 Tanaka Akira <akr@fsij.org>
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # 1. Redistributions of source code must retain the above copyright notice, this
7
+ # list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # 3. The name of the author may not be used to endorse or promote products
12
+ # derived from this software without specific prior written permission.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
19
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
22
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
23
+ # OF SUCH DAMAGE.
24
+
25
+ Tb::Cmd.subcommands << 'consecutive'
26
+
27
+ Tb::Cmd.default_option[:opt_consecutive_n] = 2
28
+
29
+ def (Tb::Cmd).op_consecutive
30
+ op = OptionParser.new
31
+ op.banner = 'Usage: tb consecutive [OPTS] [TABLE ...]'
32
+ define_common_option(op, "hNo", "--no-pager")
33
+ op.def_option('-n NUM', 'gather NUM records. (default: 2)') {|n| Tb::Cmd.opt_consecutive_n = n.to_i }
34
+ op
35
+ end
36
+
37
+ Tb::Cmd.def_vhelp('consecutive', <<'End')
38
+ Example:
39
+
40
+ % cat tst.csv
41
+ a,b,c
42
+ 0,1,2
43
+ 4,5,6
44
+ 7,8,9
45
+ % tb consecutive tstcsv
46
+ a_1,a_2,b_1,b_2,c_1,c_2
47
+ 0,4,1,5,2,6
48
+ 4,7,5,8,6,9
49
+ End
50
+
51
+ def (Tb::Cmd).main_consecutive(argv)
52
+ op_consecutive.parse!(argv)
53
+ exit_if_help('consecutive')
54
+ argv = ['-'] if argv.empty?
55
+ creader = Tb::CatReader.open(argv, Tb::Cmd.opt_N)
56
+ consecutive_header = []
57
+ creader.header.each {|f|
58
+ Tb::Cmd.opt_consecutive_n.times {|i|
59
+ consecutive_header << "#{f}_#{i+1}"
60
+ }
61
+ }
62
+ with_table_stream_output {|gen|
63
+ gen.output_header consecutive_header
64
+ buf = []
65
+ creader.each {|ary|
66
+ buf << ary
67
+ if buf.length == Tb::Cmd.opt_consecutive_n
68
+ ary2 = []
69
+ buf.each_with_index {|a, i|
70
+ a.each_with_index {|e, j|
71
+ ary2[j*Tb::Cmd.opt_consecutive_n + i] = e
72
+ }
73
+ }
74
+ gen << ary2
75
+ buf.shift
76
+ end
77
+ }
78
+ }
79
+ end
@@ -0,0 +1,105 @@
1
+ # Copyright (C) 2011 Tanaka Akira <akr@fsij.org>
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # 1. Redistributions of source code must retain the above copyright notice, this
7
+ # list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # 3. The name of the author may not be used to endorse or promote products
12
+ # derived from this software without specific prior written permission.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
19
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
22
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
23
+ # OF SUCH DAMAGE.
24
+
25
+ Tb::Cmd.subcommands << 'crop'
26
+
27
+ Tb::Cmd.default_option[:opt_crop_range] = nil
28
+
29
+ def (Tb::Cmd).op_crop
30
+ op = OptionParser.new
31
+ op.banner = 'Usage: tb crop [OPTS] [TABLE ...]'
32
+ define_common_option(op, "ho", "--no-pager")
33
+ op.def_option('-r RANGE', 'range. i.e. "2,1-4,3", "B1:D3"') {|arg| Tb::Cmd.opt_crop_range = arg }
34
+ op
35
+ end
36
+
37
+ def (Tb::Cmd).decode_a1_addressing_col(str)
38
+ (26**str.length-1)/25+str.tr("A-Z", "0-9A-P").to_i(26)
39
+ end
40
+
41
+ def (Tb::Cmd).main_crop(argv)
42
+ op_crop.parse!(argv)
43
+ exit_if_help('crop')
44
+ argv = ['-'] if argv.empty?
45
+ stream = false
46
+ if Tb::Cmd.opt_crop_range
47
+ case Tb::Cmd.opt_crop_range
48
+ when /\AR(\d+)C(\d+):R(\d+)C(\d+)\z/ # 1-based (R1C1 reference style)
49
+ stream = true
50
+ range_row1 = $1.to_i
51
+ range_col1 = $2.to_i
52
+ range_row2 = $3.to_i
53
+ range_col2 = $4.to_i
54
+ when /\A([A-Z]+)(\d+):([A-Z]+)(\d+)\z/ # 1-based (A1 reference style)
55
+ stream = true
56
+ range_col1 = decode_a1_addressing_col($1)
57
+ range_row1 = $2.to_i
58
+ range_col2 = decode_a1_addressing_col($3)
59
+ range_row2 = $4.to_i
60
+ else
61
+ raise ArgumentError, "unexpected range argument: #{Tb::Cmd.opt_crop_range.inspect}"
62
+ end
63
+ end
64
+ if stream
65
+ with_table_stream_output {|gen|
66
+ Tb::CatReader.open(argv, true) {|tblreader|
67
+ rownum = 1
68
+ tblreader.each {|ary|
69
+ if range_row2 < rownum
70
+ break
71
+ end
72
+ if range_row1 <= rownum
73
+ if range_col2 < ary.length
74
+ ary[range_col2..-1] = []
75
+ end
76
+ if 1 < range_col1
77
+ ary[0...(range_col1-1)] = []
78
+ end
79
+ gen << ary
80
+ end
81
+ rownum += 1
82
+ }
83
+ }
84
+ }
85
+ else
86
+ arys = []
87
+ Tb::CatReader.open(argv, true) {|tblreader|
88
+ tblreader.each {|a|
89
+ a.pop while !a.empty? && (a.last.nil? || a.last == '')
90
+ arys << a
91
+ }
92
+ }
93
+ arys.pop while !arys.empty? && arys.last.all? {|v| v.nil? || v == '' }
94
+ arys.shift while !arys.empty? && arys.first.all? {|v| v.nil? || v == '' }
95
+ if !arys.empty?
96
+ while arys.all? {|a| a.empty? || (a.first.nil? || a.first == '') }
97
+ arys.each {|a| a.shift }
98
+ end
99
+ end
100
+ with_table_stream_output {|gen|
101
+ arys.each {|a| gen << a }
102
+ }
103
+ end
104
+ end
105
+
@@ -0,0 +1,119 @@
1
+ # Copyright (C) 2011 Tanaka Akira <akr@fsij.org>
2
+ #
3
+ # Redistribution and use in source and binary forms, with or without
4
+ # modification, are permitted provided that the following conditions are met:
5
+ #
6
+ # 1. Redistributions of source code must retain the above copyright notice, this
7
+ # list of conditions and the following disclaimer.
8
+ # 2. Redistributions in binary form must reproduce the above copyright notice,
9
+ # this list of conditions and the following disclaimer in the documentation
10
+ # and/or other materials provided with the distribution.
11
+ # 3. The name of the author may not be used to endorse or promote products
12
+ # derived from this software without specific prior written permission.
13
+ #
14
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED
15
+ # WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
16
+ # MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO
17
+ # EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
18
+ # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT
19
+ # OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21
+ # CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
22
+ # IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY
23
+ # OF SUCH DAMAGE.
24
+
25
+ Tb::Cmd.subcommands << 'cross'
26
+
27
+ Tb::Cmd.default_option[:opt_cross_fields] = []
28
+ Tb::Cmd.default_option[:opt_cross_compact] = false
29
+
30
+ def (Tb::Cmd).op_cross
31
+ op = OptionParser.new
32
+ op.banner = 'Usage: tb cross [OPTS] HKEY-FIELD1,... VKEY-FIELD1,... [TABLE ...]'
33
+ define_common_option(op, "ho", "--no-pager")
34
+ op.def_option('-a AGGREGATION-SPEC[,NEW-FIELD]',
35
+ '--aggregate AGGREGATION-SPEC[,NEW-FIELD]') {|arg| Tb::Cmd.opt_cross_fields << arg }
36
+ op.def_option('-c', '--compact', 'compact format') { Tb::Cmd.opt_cross_compact = true }
37
+ op
38
+ end
39
+
40
+ def (Tb::Cmd).main_cross(argv)
41
+ op_cross.parse!(argv)
42
+ exit_if_help('cross')
43
+ err('no hkey-fields given.') if argv.empty?
44
+ hkfs = split_field_list_argument(argv.shift)
45
+ err('no vkey-fields given.') if argv.empty?
46
+ vkfs = split_field_list_argument(argv.shift)
47
+ if Tb::Cmd.opt_cross_fields.empty?
48
+ opt_cross_fields = [['count', 'count']]
49
+ else
50
+ opt_cross_fields = Tb::Cmd.opt_cross_fields.map {|arg|
51
+ agg_spec, new_field = split_field_list_argument(arg)
52
+ new_field ||= agg_spec
53
+ [agg_spec, new_field]
54
+ }
55
+ end
56
+ argv = ['-'] if argv.empty?
57
+ Tb::CatReader.open(argv, Tb::Cmd.opt_N) {|tblreader|
58
+ vkis = vkfs.map {|f| tblreader.index_from_field(f) }
59
+ hkis = hkfs.map {|f| tblreader.index_from_field(f) }
60
+ vset = {}
61
+ hset = {}
62
+ set = {}
63
+ tblreader.each {|ary|
64
+ vkvs = ary.values_at(*vkis)
65
+ hkvs = ary.values_at(*hkis)
66
+ vset[vkvs] = true if !vset.include?(vkvs)
67
+ hset[hkvs] = true if !hset.include?(hkvs)
68
+ if !set.include?([vkvs, hkvs])
69
+ set[[vkvs, hkvs]] = opt_cross_fields.map {|agg_spec, nf|
70
+ begin
71
+ ag = make_aggregator(agg_spec, tblreader.header)
72
+ rescue ArgumentError
73
+ err($!.message)
74
+ end
75
+ ag.update(ary)
76
+ ag
77
+ }
78
+ else
79
+ set[[vkvs, hkvs]].each {|ag|
80
+ ag.update(ary)
81
+ }
82
+ end
83
+ }
84
+ vary = vset.keys.sort_by {|a| a.map {|v| smart_cmp_value(v) } }
85
+ hary = hset.keys.sort_by {|a| a.map {|v| smart_cmp_value(v) } }
86
+ with_output {|out|
87
+ Tb.csv_stream_output(out) {|gen|
88
+ hkfs.each_with_index {|hkf, i|
89
+ next if Tb::Cmd.opt_cross_compact && i == hkfs.length - 1
90
+ row = [nil] * (vkfs.length - 1) + [hkf]
91
+ hary.each {|hkvs| opt_cross_fields.length.times { row << hkvs[i] } }
92
+ gen << row
93
+ }
94
+ if Tb::Cmd.opt_cross_compact
95
+ r = vkfs.dup
96
+ hary.each {|hkvs| r.concat([hkvs[-1]] * opt_cross_fields.length) }
97
+ gen << r
98
+ else
99
+ r = vkfs.dup
100
+ hary.each {|hkvs| r.concat opt_cross_fields.map {|agg_spec, new_field| new_field } }
101
+ gen << r
102
+ end
103
+ vary.each {|vkvs|
104
+ row = vkvs.dup
105
+ hary.each {|hkvs|
106
+ ags = set[[vkvs, hkvs]]
107
+ if !ags
108
+ opt_cross_fields.length.times { row << nil }
109
+ else
110
+ ags.each {|ag| row << ag.finish }
111
+ end
112
+ }
113
+ gen << row
114
+ }
115
+ }
116
+ }
117
+ }
118
+ end
119
+