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) 2012 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
@@ -28,23 +28,25 @@
28
28
 
29
29
  require 'json'
30
30
 
31
- class Tb
32
- class JSONReader
33
- include Tb::Enumerable
34
-
35
- def initialize(string)
36
- @ary = JSON.parse(string)
31
+ module Tb
32
+ class JSONReader < Tb::HashReader
33
+ def initialize(io)
34
+ ary = JSON.parse(io.read)
35
+ super lambda { ary.shift }
37
36
  end
37
+ end
38
38
 
39
- def header_and_each(header_proc)
40
- header_proc.call(nil) if header_proc
41
- @ary.each {|obj|
42
- yield obj
39
+ class JSONWriter < Tb::HashWriter
40
+ def initialize(io)
41
+ io << "[\n"
42
+ sep = ""
43
+ super lambda {|hash|
44
+ io << sep << JSON.pretty_generate(hash)
45
+ sep = ",\n"
46
+ },
47
+ lambda {
48
+ io << "\n]\n"
43
49
  }
44
50
  end
45
-
46
- def each(&block)
47
- header_and_each(nil, &block)
48
- end
49
51
  end
50
52
  end
@@ -1,6 +1,6 @@
1
1
  # lib/tb/ltsv.rb - LTSV related fetures for table library
2
2
  #
3
- # Copyright (C) 2013 Tanaka Akira <akr@fsij.org>
3
+ # Copyright (C) 2013-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,38 +30,7 @@
30
30
 
31
31
  require 'stringio'
32
32
 
33
- class Tb
34
- def Tb.load_ltsv(filename, &block)
35
- Tb.parse_ltsv(File.read(filename), &block)
36
- end
37
-
38
- def Tb.parse_ltsv(ltsv)
39
- assoc_list = []
40
- ltsv_stream_input(ltsv) {|assoc|
41
- assoc_list << assoc
42
- }
43
- fields_hash = {}
44
- assoc_list.each {|assoc|
45
- assoc.each {|key, val|
46
- fields_hash[key] ||= fields_hash.size
47
- }
48
- }
49
- header = fields_hash.keys
50
- t = Tb.new(header)
51
- assoc_list.each {|assoc|
52
- t.insert Hash[assoc]
53
- }
54
- t
55
- end
56
-
57
- def Tb.ltsv_stream_input(ltsv)
58
- ltsvreader = LTSVReader.new(ltsv)
59
- while assoc = ltsvreader.shift
60
- yield assoc
61
- end
62
- nil
63
- end
64
-
33
+ module Tb
65
34
  def Tb.ltsv_escape_key(str)
66
35
  if /[\0-\x1f":\\\x7f]/ =~ str
67
36
  '"' +
@@ -138,70 +107,19 @@ class Tb
138
107
  end
139
108
  end
140
109
 
141
- class LTSVReader
142
- include Tb::Enumerable
143
-
144
- def initialize(input)
145
- if input.respond_to? :to_str
146
- @input = StringIO.new(input)
147
- else
148
- @input = input
149
- end
150
- end
151
-
152
- def shift
153
- line = @input.gets
154
- return nil if !line
155
- line = line.chomp("\n")
156
- line = line.chomp("\r")
157
- ary = line.split(/\t/, -1)
158
- assoc = ary.map {|str|
159
- /:/ =~ str
160
- key = $`
161
- val = $'
162
- key = Tb.ltsv_unescape_string(key)
163
- val = Tb.ltsv_unescape_string(val)
164
- [key, val]
165
- }
166
- assoc
167
- end
168
-
169
- def header_and_each(header_proc)
170
- header_proc.call(nil) if header_proc
171
- while assoc = self.shift
172
- yield Hash[assoc]
173
- end
174
- end
175
-
176
- def each(&block)
177
- header_and_each(nil, &block)
178
- end
179
- end
180
-
181
- def Tb.ltsv_stream_output(out)
182
- gen = Object.new
183
- gen.instance_variable_set(:@out, out)
184
- def gen.<<(assoc)
185
- @out << Tb.ltsv_assoc_join(assoc) << "\n"
186
- end
187
- yield gen
188
- end
189
-
190
- # :call-seq:
191
- # generate_ltsv(out='') {|recordids| modified_recordids }
192
- # generate_ltsv(out='')
193
- #
194
- def generate_ltsv(out='', fields=nil, &block)
195
- recordids = list_recordids
196
- if block_given?
197
- recordids = yield(recordids)
198
- end
199
- Tb.ltsv_stream_output(out) {|gen|
200
- recordids.each {|recordid|
201
- gen << get_record(recordid)
202
- }
110
+ def Tb.ltsv_split_line(line)
111
+ line = line.chomp("\n")
112
+ line = line.chomp("\r")
113
+ ary = line.split(/\t/, -1)
114
+ assoc = ary.map {|str|
115
+ /:/ =~ str
116
+ key = $`
117
+ val = $'
118
+ key = Tb.ltsv_unescape_string(key)
119
+ val = Tb.ltsv_unescape_string(val)
120
+ [key, val]
203
121
  }
204
- out
122
+ assoc
205
123
  end
206
124
 
207
125
  def Tb.ltsv_assoc_join(assoc)
@@ -209,4 +127,25 @@ class Tb
209
127
  Tb.ltsv_escape_key(key) + ':' + Tb.ltsv_escape_value(val)
210
128
  }.join("\t")
211
129
  end
130
+
131
+ class LTSVReader < Tb::HashReader
132
+ def initialize(io)
133
+ super lambda {
134
+ line = io.gets
135
+ if line
136
+ Hash[Tb.ltsv_split_line(line)]
137
+ else
138
+ nil
139
+ end
140
+ }
141
+ end
142
+ end
143
+
144
+ class LTSVWriter < Tb::HashWriter
145
+ def initialize(io)
146
+ super lambda {|hash|
147
+ io << (Tb.ltsv_assoc_join(hash) + "\n")
148
+ }
149
+ end
150
+ end
212
151
  end
@@ -0,0 +1,63 @@
1
+ # Copyright (C) 2014 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
5
+ # are met:
6
+ #
7
+ # 1. Redistributions of source code must retain the above copyright
8
+ # notice, this list of conditions and the following disclaimer.
9
+ # 2. Redistributions in binary form must reproduce the above
10
+ # copyright notice, this list of conditions and the following
11
+ # disclaimer in the documentation and/or other materials provided
12
+ # with the distribution.
13
+ # 3. The name of the author may not be used to endorse or promote
14
+ # products derived from this software without specific prior
15
+ # written permission.
16
+ #
17
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
18
+ # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
20
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
21
+ # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
22
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
23
+ # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
24
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
25
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
26
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
27
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
+
29
+ require 'json'
30
+
31
+ # NDJSON is Newline Delimited JSON
32
+ # http://ndjson.org/
33
+ #
34
+ # Tb::NDJSONReader accepts empty lines.
35
+
36
+ module Tb
37
+ class NDJSONReader < Tb::HashReader
38
+ # io.gets should returns a string.
39
+ def initialize(io)
40
+ super lambda {
41
+ while true
42
+ line = io.gets
43
+ if line.nil? || /\S/ =~ line
44
+ break
45
+ end
46
+ end
47
+ if line
48
+ JSON.parse(line)
49
+ else
50
+ nil
51
+ end
52
+ }
53
+ end
54
+ end
55
+
56
+ class NDJSONWriter < Tb::HashWriter
57
+ def initialize(io)
58
+ super lambda {|hash|
59
+ io << (JSON.generate(hash) + "\n")
60
+ }
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,66 @@
1
+ # lib/tb/numericreaderm.rb - reader mixin for table without header
2
+ #
3
+ # Copyright (C) 2014 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
7
+ # are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ # 2. Redistributions in binary form must reproduce the above
12
+ # copyright notice, this list of conditions and the following
13
+ # disclaimer in the documentation and/or other materials provided
14
+ # with the distribution.
15
+ # 3. The name of the author may not be used to endorse or promote
16
+ # products derived from this software without specific prior
17
+ # written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
20
+ # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
23
+ # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
25
+ # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
27
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
28
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
29
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ class Tb::NumericReader
32
+ include Tb::EnumerableWithEach
33
+
34
+ def initialize(get_array)
35
+ @get_array = get_array
36
+ end
37
+
38
+ def header_known?
39
+ false
40
+ end
41
+
42
+ def get_named_header
43
+ []
44
+ end
45
+
46
+ def get_hash
47
+ ary = @get_array.call
48
+ if !ary
49
+ return nil
50
+ end
51
+ hash = {}
52
+ ary.each_with_index {|v, i|
53
+ field = (i+1).to_s
54
+ hash[field] = v
55
+ }
56
+ hash
57
+ end
58
+
59
+ def header_and_each(header_proc)
60
+ header_proc.call(get_named_header) if header_proc
61
+ while hash = get_hash
62
+ yield hash
63
+ end
64
+ nil
65
+ end
66
+ end
@@ -0,0 +1,61 @@
1
+ # lib/tb/numericwriterm.rb - writer mixin for table without header
2
+ #
3
+ # Copyright (C) 2014 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
7
+ # are met:
8
+ #
9
+ # 1. Redistributions of source code must retain the above copyright
10
+ # notice, this list of conditions and the following disclaimer.
11
+ # 2. Redistributions in binary form must reproduce the above
12
+ # copyright notice, this list of conditions and the following
13
+ # disclaimer in the documentation and/or other materials provided
14
+ # with the distribution.
15
+ # 3. The name of the author may not be used to endorse or promote
16
+ # products derived from this software without specific prior
17
+ # written permission.
18
+ #
19
+ # THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS
20
+ # OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
21
+ # WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22
+ # ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
23
+ # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
24
+ # DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
25
+ # GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26
+ # INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
27
+ # WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
28
+ # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
29
+ # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
+
31
+ require 'tempfile'
32
+
33
+ class Tb::NumericWriter
34
+ def initialize(put_array, put_finish=nil)
35
+ @put_array = put_array
36
+ @put_finish = put_finish
37
+ end
38
+
39
+ def header_required?
40
+ false
41
+ end
42
+
43
+ def header_generator=(gen)
44
+ end
45
+
46
+ def put_hash(hash)
47
+ ary = []
48
+ hash.each {|k, v|
49
+ if /\A[1-9][0-9]*\z/ !~ k
50
+ raise ArgumentError, "numeric field name expected: #{k.inspect}"
51
+ end
52
+ ary[k.to_i-1] = v
53
+ }
54
+ @put_array.call ary
55
+ nil
56
+ end
57
+
58
+ def finish
59
+ @put_finish.call if @put_finish
60
+ end
61
+ end
@@ -1,6 +1,6 @@
1
1
  # lib/tb/pnm.rb - tools for (very small) PNM images.
2
2
  #
3
- # Copyright (C) 2010-2012 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
@@ -28,43 +28,47 @@
28
28
  # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
29
29
  # EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30
30
 
31
- class Tb
32
- # :call-seq:
33
- # Tb.load_pnm(pnm_filename) -> tb
34
- #
35
- def Tb.load_pnm(pnm_filename)
36
- pnm_content = File.open(pnm_filename, "rb") {|f| f.read }
37
- Tb.parse_pnm(pnm_content)
38
- end
31
+ module Tb
32
+ # practical only for (very) small images.
33
+ class PNMReader < HashReader
34
+ def initialize(pnm_io)
35
+ pnm_io.binmode
36
+ content = pnm_io.read
37
+ initialize0(content)
38
+ header = @ary.shift
39
+ super lambda {
40
+ a = @ary.shift
41
+ if a
42
+ h = {}
43
+ a.each_with_index {|v, i|
44
+ h[header[i]] = v if !v.nil?
45
+ }
46
+ h
47
+ else
48
+ nil
49
+ end
50
+ }
51
+ end
39
52
 
40
- # :call-seq:
41
- # Tb.parse_pnm(pnm_content) -> tb
42
- #
43
- def Tb.parse_pnm(pnm_content)
44
- reader = PNMReader.new(pnm_content)
45
- header = reader.shift
46
- t = Tb.new(header)
47
- reader.each {|ary|
48
- t.insert_values header, ary
49
- }
50
- t
51
- end
53
+ def header_known?
54
+ true
55
+ end
52
56
 
53
- # :call-seq:
54
- # Tb.pnm_stream_input(pnm_io) {|ary| ... }
55
- #
56
- def Tb.pnm_stream_input(pnm_io)
57
- pnm_io.binmode
58
- content = pnm_io.read
59
- PNMReader.new(content)
60
- end
57
+ def get_named_header
58
+ ['type', 'x', 'y', 'component', 'value']
59
+ end
61
60
 
62
- # practical only for (very) small images.
63
- class PNMReader
64
61
  WSP = /(?:[ \t\r\n]|\#[^\r\n]*[\r\n])+/
65
62
 
66
- def initialize(pnm_content)
67
- pnm_content.force_encoding("ASCII-8BIT") if pnm_content.respond_to? :force_encoding
63
+ def initialize0(pnm_content)
64
+ if pnm_content.respond_to? :to_str
65
+ pnm_content.force_encoding("ASCII-8BIT") if pnm_content.respond_to? :force_encoding
66
+ else
67
+ # IO
68
+ pnm_content.binmode
69
+ pnm_content = pnm_content.read
70
+ end
71
+
68
72
  if /\A(P[63])(#{WSP})(\d+)(#{WSP})(\d+)(#{WSP})(\d+)[ \t\r\n]/on =~ pnm_content
69
73
  magic, wsp1, w, wsp2, h, wsp3, max, raster = $1, $2, $3.to_i, $4, $5.to_i, $6, $7.to_i, $'
70
74
  pixel_component = %w[R G B]
@@ -162,197 +166,199 @@ class Tb
162
166
  end
163
167
  iter
164
168
  end
169
+ end
165
170
 
166
- def shift
167
- @ary.shift
171
+ class PNMWriter
172
+ def initialize(io)
173
+ @io = io
174
+ @ary = []
175
+ @fields = {}
168
176
  end
169
177
 
170
- def each
171
- while ary = self.shift
172
- yield ary
173
- end
174
- nil
178
+ def header_required?
179
+ false
175
180
  end
176
181
 
177
- def to_a
178
- result= []
179
- each {|ary| result << ary }
180
- result
182
+ def header_generator=(gen)
181
183
  end
182
- end
183
184
 
184
- # :call-seq:
185
- # generate_pnm(out='')
186
- #
187
- def generate_pnm(out='')
188
- undefined_field = ['x', 'y', 'component', 'value'] - self.list_fields
189
- if !undefined_field.empty?
190
- raise ArgumentError, "field not defined: #{undefined_field.inspect[1...-1]}"
185
+ def put_hash(pairs)
186
+ @ary << pairs
187
+ pairs.each {|k, v|
188
+ @fields[k] = true
189
+ }
191
190
  end
192
- pnm_type = nil
193
- width = height = nil
194
- comments = []
195
- max_value = nil
196
- max_x = max_y = 0
197
- values = { 0.0 => true, 1.0 => true }
198
- components = {}
199
- self.each {|rec|
200
- case rec['component']
201
- when 'pnm_type'
202
- pnm_type = rec['value']
203
- when 'width'
204
- width = rec['value'].to_i
205
- when 'height'
206
- height = rec['value'].to_i
207
- when 'max'
208
- max_value = rec['value'].to_i
209
- when 'comment'
210
- comments << rec['value']
211
- when 'R', 'G', 'B', 'V'
212
- components[rec['component']] = true
213
- x = rec['x'].to_i
214
- y = rec['y'].to_i
215
- max_x = x if max_x < x
216
- max_y = y if max_y < y
217
- values[rec['value'].to_f] = true
218
- end
219
191
 
220
- }
221
- if !(components.keys - %w[V]).empty? &&
222
- !(components.keys - %w[R G B]).empty?
223
- raise ArgumentError, "inconsistent color component: #{components.keys.sort.inspect[1...-1]}"
224
- end
225
- case pnm_type
226
- when 'P1', 'P4' then raise ArgumentError, "unexpected compoenent for PBM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
227
- when 'P2', 'P5' then raise ArgumentError, "unexpected compoenent for PGM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
228
- when 'P3', 'P6' then raise ArgumentError, "unexpected compoenent for PPM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[R G B]).empty?
229
- end
230
- comments.each {|c|
231
- if /[\r\n]/ =~ c
232
- raise ArgumentError, "comment cannot contain a newline: #{c.inspect}"
192
+ def finish
193
+ undefined_field = ['x', 'y', 'component', 'value'] - @fields.keys
194
+ if !undefined_field.empty?
195
+ raise ArgumentError, "field not defined: #{undefined_field.inspect[1...-1]}"
233
196
  end
234
- }
235
- if !width
236
- width = max_x + 1
237
- end
238
- if !height
239
- height = max_y + 1
240
- end
241
- if !max_value
242
- min_interval = 1.0
243
- values.keys.sort.each_cons(2) {|v1, v2|
244
- d = v2-v1
245
- min_interval = d if d < min_interval
197
+ pnm_type = nil
198
+ width = height = nil
199
+ comments = []
200
+ max_value = nil
201
+ max_x = max_y = 0
202
+ values = { 0.0 => true, 1.0 => true }
203
+ components = {}
204
+ @ary.each {|rec|
205
+ case rec['component']
206
+ when 'pnm_type'
207
+ pnm_type = rec['value']
208
+ when 'width'
209
+ width = rec['value'].to_i
210
+ when 'height'
211
+ height = rec['value'].to_i
212
+ when 'max'
213
+ max_value = rec['value'].to_i
214
+ when 'comment'
215
+ comments << rec['value']
216
+ when 'R', 'G', 'B', 'V'
217
+ components[rec['component']] = true
218
+ x = rec['x'].to_i
219
+ y = rec['y'].to_i
220
+ max_x = x if max_x < x
221
+ max_y = y if max_y < y
222
+ values[rec['value'].to_f] = true
223
+ end
246
224
  }
247
- if min_interval < 0.0039 # 1/255 = 0.00392156862745098...
248
- max_value = 0xffff
249
- elsif min_interval < 1.0 || !(components.keys & %w[R G B]).empty?
250
- max_value = 0xff
251
- else
252
- max_value = 1
225
+ if !(components.keys - %w[V]).empty? &&
226
+ !(components.keys - %w[R G B]).empty?
227
+ raise ArgumentError, "inconsistent color component: #{components.keys.sort.inspect[1...-1]}"
253
228
  end
254
- end
255
- if pnm_type
256
- if !pnm_type.kind_of?(String) || /\AP[123456]\z/ !~ pnm_type
257
- raise ArgumentError, "unexpected PNM type: #{pnm_type.inspect}"
229
+ case pnm_type
230
+ when 'P1', 'P4' then raise ArgumentError, "unexpected compoenent for PBM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
231
+ when 'P2', 'P5' then raise ArgumentError, "unexpected compoenent for PGM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[V]).empty?
232
+ when 'P3', 'P6' then raise ArgumentError, "unexpected compoenent for PPM: #{components.keys.sort.inspect[1...-1]}" if !(components.keys - %w[R G B]).empty?
258
233
  end
259
- else
260
- if (components.keys - ['V']).empty?
261
- if max_value == 1
262
- pnm_type = 'P4' # PBM
263
- else
264
- pnm_type = 'P5' # PGM
234
+ comments.each {|c|
235
+ if /[\r\n]/ =~ c
236
+ raise ArgumentError, "comment cannot contain a newline: #{c.inspect}"
265
237
  end
266
- else
267
- pnm_type = 'P6' # PPM
238
+ }
239
+ if !width
240
+ width = max_x + 1
268
241
  end
269
- end
270
- header = "#{pnm_type}\n"
271
- comments.each {|c| header << '#' << c << "\n" }
272
- header << "#{width} #{height}\n"
273
- header << "#{max_value}\n" if /P[2536]/ =~ pnm_type
274
- if /P[14]/ =~ pnm_type # PBM
275
- max_value = 1
276
- end
277
- bytes_per_component = bytes_per_line = component_fmt = component_template = nil
278
- case pnm_type
279
- when 'P1' then bytes_per_component = 1; raster = '1' * (width * height)
280
- when 'P4' then bytes_per_line = (width + 7) / 8; raster = ["1"*width].pack("B*") * height
281
- when 'P2' then bytes_per_component = max_value.to_s.length+1; component_fmt = "%#{bytes_per_component}d"; raster = (component_fmt % 0) * (width * height)
282
- when 'P5' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * width * height)
283
- when 'P3' then bytes_per_component = max_value.to_s.length+1; component_fmt = "%#{bytes_per_component}d"; raster = (component_fmt % 0) * (3 * width * height)
284
- when 'P6' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * 3 * width * height)
285
- else
286
- raise
287
- end
288
- raster.force_encoding("ASCII-8BIT") if raster.respond_to? :force_encoding
289
- self.each {|rec|
290
- c = rec['component']
291
- next if /\A[RGBV]\z/ !~ c
292
- x = rec['x'].to_i
293
- y = rec['y'].to_i
294
- next if x < 0 || width <= x
295
- next if y < 0 || height <= y
296
- v = rec['value'].to_f
297
- if v < 0
298
- v = 0
299
- elsif 1 < v
300
- v = 1
242
+ if !height
243
+ height = max_y + 1
301
244
  end
302
- case pnm_type
303
- when 'P1'
304
- v = v < 0.5 ? '1' : '0'
305
- raster[y * width + x] = v
306
- when 'P4'
307
- xhi, xlo = x.divmod(8)
308
- i = y * bytes_per_line + xhi
309
- byte = raster[i].ord
310
- if v < 0.5
311
- byte |= 0x80 >> xlo
245
+ if !max_value
246
+ min_interval = 1.0
247
+ values.keys.sort.each_cons(2) {|v1, v2|
248
+ d = v2-v1
249
+ min_interval = d if d < min_interval
250
+ }
251
+ if min_interval < 0.0039 # 1/255 = 0.00392156862745098...
252
+ max_value = 0xffff
253
+ elsif min_interval < 1.0 || !(components.keys & %w[R G B]).empty?
254
+ max_value = 0xff
312
255
  else
313
- byte &= 0xff7f >> xlo
256
+ max_value = 1
314
257
  end
315
- raster[i] = [byte].pack("C")
316
- when 'P2'
317
- v = (v * max_value).round
318
- raster[(y * width + x) * bytes_per_component, bytes_per_component] = component_fmt % v
319
- when 'P5'
320
- v = (v * max_value).round
321
- raster[(y * width + x) * bytes_per_component, bytes_per_component] = [v].pack(component_template)
322
- when 'P3'
323
- v = (v * max_value).round
324
- i = (y * width + x) * 3
325
- if c == 'G' then i += 1
326
- elsif c == 'B' then i += 2
258
+ end
259
+ if pnm_type
260
+ if !pnm_type.kind_of?(String) || /\AP[123456]\z/ !~ pnm_type
261
+ raise ArgumentError, "unexpected PNM type: #{pnm_type.inspect}"
327
262
  end
328
- raster[i * bytes_per_component, bytes_per_component] = component_fmt % v
329
- when 'P6'
330
- v = (v * max_value).round
331
- i = (y * width + x) * 3
332
- if c == 'G' then i += 1
333
- elsif c == 'B' then i += 2
263
+ else
264
+ if (components.keys - ['V']).empty?
265
+ if max_value == 1
266
+ pnm_type = 'P4' # PBM
267
+ else
268
+ pnm_type = 'P5' # PGM
269
+ end
270
+ else
271
+ pnm_type = 'P6' # PPM
334
272
  end
335
- raster[i * bytes_per_component, bytes_per_component] = [v].pack(component_template)
273
+ end
274
+ header = "#{pnm_type}\n"
275
+ comments.each {|c| header << '#' << c << "\n" }
276
+ header << "#{width} #{height}\n"
277
+ header << "#{max_value}\n" if /P[2536]/ =~ pnm_type
278
+ if /P[14]/ =~ pnm_type # PBM
279
+ max_value = 1
280
+ end
281
+ bytes_per_component = bytes_per_line = component_fmt = component_template = nil
282
+ case pnm_type
283
+ when 'P1' then bytes_per_component = 1; raster = '1' * (width * height)
284
+ when 'P4' then bytes_per_line = (width + 7) / 8; raster = ["1"*width].pack("B*") * height
285
+ when 'P2' then bytes_per_component = max_value.to_s.length+1; component_fmt = "%#{bytes_per_component}d"; raster = (component_fmt % 0) * (width * height)
286
+ when 'P5' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * width * height)
287
+ when 'P3' then bytes_per_component = max_value.to_s.length+1; component_fmt = "%#{bytes_per_component}d"; raster = (component_fmt % 0) * (3 * width * height)
288
+ when 'P6' then bytes_per_component, component_template = max_value < 0x100 ? [1, 'C'] : [2, 'n']; raster = "\0" * (bytes_per_component * 3 * width * height)
336
289
  else
337
290
  raise
338
291
  end
339
- }
340
- if pnm_type == 'P1'
341
- raster.gsub!(/[01]{#{width}}/, "\\&\n")
342
- if 70 < width
343
- raster.gsub!(/[01]{70}/, "\\&\n")
344
- end
345
- raster << "\n" if /\n\z/ !~ raster
346
- elsif /P[23]/ =~ pnm_type
347
- components_per_line = /P2/ =~ pnm_type ? width : 3 * width
348
- raster.gsub!(/ +/, ' ')
349
- raster.gsub!(/( \d+){#{components_per_line}}/, "\\&\n")
350
- raster.gsub!(/(\A|\n) +/, '\1')
351
- raster.gsub!(/.{71,}\n/) {
352
- $&.gsub(/(.{1,69})[ \n]/, "\\1\n")
292
+ raster.force_encoding("ASCII-8BIT") if raster.respond_to? :force_encoding
293
+ @ary.each {|rec|
294
+ c = rec['component']
295
+ next if /\A[RGBV]\z/ !~ c
296
+ x = rec['x'].to_i
297
+ y = rec['y'].to_i
298
+ next if x < 0 || width <= x
299
+ next if y < 0 || height <= y
300
+ v = rec['value'].to_f
301
+ if v < 0
302
+ v = 0
303
+ elsif 1 < v
304
+ v = 1
305
+ end
306
+ case pnm_type
307
+ when 'P1'
308
+ v = v < 0.5 ? '1' : '0'
309
+ raster[y * width + x] = v
310
+ when 'P4'
311
+ xhi, xlo = x.divmod(8)
312
+ i = y * bytes_per_line + xhi
313
+ byte = raster[i].ord
314
+ if v < 0.5
315
+ byte |= 0x80 >> xlo
316
+ else
317
+ byte &= 0xff7f >> xlo
318
+ end
319
+ raster[i] = [byte].pack("C")
320
+ when 'P2'
321
+ v = (v * max_value).round
322
+ raster[(y * width + x) * bytes_per_component, bytes_per_component] = component_fmt % v
323
+ when 'P5'
324
+ v = (v * max_value).round
325
+ raster[(y * width + x) * bytes_per_component, bytes_per_component] = [v].pack(component_template)
326
+ when 'P3'
327
+ v = (v * max_value).round
328
+ i = (y * width + x) * 3
329
+ if c == 'G' then i += 1
330
+ elsif c == 'B' then i += 2
331
+ end
332
+ raster[i * bytes_per_component, bytes_per_component] = component_fmt % v
333
+ when 'P6'
334
+ v = (v * max_value).round
335
+ i = (y * width + x) * 3
336
+ if c == 'G' then i += 1
337
+ elsif c == 'B' then i += 2
338
+ end
339
+ raster[i * bytes_per_component, bytes_per_component] = [v].pack(component_template)
340
+ else
341
+ raise
342
+ end
353
343
  }
354
- raster << "\n" if /\n\z/ !~ raster
344
+ if pnm_type == 'P1'
345
+ raster.gsub!(/[01]{#{width}}/, "\\&\n")
346
+ if 70 < width
347
+ raster.gsub!(/[01]{70}/, "\\&\n")
348
+ end
349
+ raster << "\n" if /\n\z/ !~ raster
350
+ elsif /P[23]/ =~ pnm_type
351
+ components_per_line = /P2/ =~ pnm_type ? width : 3 * width
352
+ raster.gsub!(/ +/, ' ')
353
+ raster.gsub!(/( \d+){#{components_per_line}}/, "\\&\n")
354
+ raster.gsub!(/(\A|\n) +/, '\1')
355
+ raster.gsub!(/.{71,}\n/) {
356
+ $&.gsub(/(.{1,69})[ \n]/, "\\1\n")
357
+ }
358
+ raster << "\n" if /\n\z/ !~ raster
359
+ end
360
+ @io << (header+raster)
355
361
  end
356
- out << (header+raster)
357
362
  end
363
+
358
364
  end