columnist 1.0.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.
data/lib/columnist.rb ADDED
@@ -0,0 +1,173 @@
1
+ require 'stringio'
2
+
3
+ require 'columnist/options_validator'
4
+ require 'columnist/formatter/progress'
5
+ require 'columnist/formatter/nested'
6
+ require 'columnist/row'
7
+ require 'columnist/column'
8
+ require 'columnist/table'
9
+
10
+ module Columnist
11
+ include OptionsValidator
12
+
13
+ attr_reader :formatter
14
+
15
+ DEFAULTS = {
16
+ :width => 100,
17
+ :align => 'left',
18
+ :formatter => 'nested',
19
+ :encoding => :unicode,
20
+ }
21
+
22
+ def capture_output
23
+ $stdout.rewind
24
+ $stdout.read
25
+ ensure
26
+ $stdout = STDOUT
27
+ end
28
+
29
+ def suppress_output
30
+ $stdout = StringIO.new
31
+ end
32
+
33
+ def restore_output
34
+ $stdout = STDOUT
35
+ end
36
+
37
+ def formatter=(type = 'nested')
38
+ name = type.capitalize + 'Formatter'
39
+ klass = %W{Columnist #{name}}.inject(Kernel) {|s,c| s.const_get(c)}
40
+
41
+ # Each formatter is a singleton that responds to #instance
42
+ @formatter = klass.instance
43
+ rescue
44
+ raise ArgumentError, 'Invalid formatter specified'
45
+ end
46
+
47
+ def header(options = {})
48
+ section(:header, options)
49
+ end
50
+
51
+ def report(options = {}, &block)
52
+ self.formatter ||= DEFAULTS[:formatter]
53
+ self.formatter.format(options, block)
54
+ end
55
+
56
+ def progress(override = nil)
57
+ self.formatter.progress(override)
58
+ end
59
+
60
+ def footer(options = {})
61
+ section(:footer, options)
62
+ end
63
+
64
+ def horizontal_rule(options = {})
65
+ validate_options(options, :char, :width, :color, :bold)
66
+
67
+ # Got unicode?
68
+ use_char = "\u2501" == 'u2501' ? '-' : "\u2501"
69
+
70
+ char = options[:char].is_a?(String) ? options[:char] : use_char
71
+ width = options[:width] || DEFAULTS[:width]
72
+
73
+ aligned(char * width, :width => width, :color => options[:color], :bold => options[:bold])
74
+ end
75
+
76
+ def vertical_spacing(lines = 1)
77
+ puts "\n" * lines
78
+ rescue
79
+ raise ArgumentError
80
+ end
81
+
82
+ def datetime(options = {})
83
+ validate_options(options, :align, :width, :format, :color, :bold)
84
+
85
+ format = options[:format] || '%Y-%m-%d - %l:%M:%S%p'
86
+ align = options[:align] || DEFAULTS[:align]
87
+ width = options[:width] || DEFAULTS[:width]
88
+
89
+ text = Time.now.strftime(format)
90
+
91
+ raise Exception if text.size > width
92
+
93
+ aligned(text, :align => align, :width => width, :color => options[:color], :bold => options[:bold])
94
+ end
95
+
96
+ def aligned(text, options = {})
97
+ validate_options(options, :align, :width, :color, :bold)
98
+
99
+ align = options[:align] || DEFAULTS[:align]
100
+ width = options[:width] || DEFAULTS[:width]
101
+ color = options[:color]
102
+ bold = options[:bold] || false
103
+
104
+ line = case align
105
+ when 'left'
106
+ text
107
+ when 'right'
108
+ text.rjust(width)
109
+ when 'center'
110
+ text.rjust((width - text.size)/2 + text.size)
111
+ else
112
+ raise ArgumentError
113
+ end
114
+
115
+ line = line.send(color) if color
116
+ line = line.send('bold') if bold
117
+
118
+ puts line
119
+ end
120
+
121
+ def table(options = {})
122
+ @table = Columnist::Table.new(options)
123
+ yield
124
+ @table.output
125
+ end
126
+
127
+ def row(options = {})
128
+ options[:encoding] ||= @table.encoding
129
+ @row = Columnist::Row.new(options)
130
+ yield
131
+ @table.add(@row)
132
+ end
133
+
134
+ def column(text, options = {})
135
+ col = Columnist::Column.new(text, options)
136
+ @row.add(col)
137
+ end
138
+
139
+ private
140
+
141
+ def section(type, options)
142
+ title, width, align, lines, color, bold = assign_section_properties(options)
143
+
144
+ # This also ensures that width is a Fixnum
145
+ raise ArgumentError if title.size > width
146
+
147
+ if type == :footer
148
+ vertical_spacing(lines)
149
+ horizontal_rule(:char => options[:rule], :width => width, :color => color, :bold => bold) if options[:rule]
150
+ end
151
+
152
+ aligned(title, :align => align, :width => width, :color => color, :bold => bold)
153
+ datetime(:align => align, :width => width, :color => color, :bold => bold) if options[:timestamp]
154
+
155
+ if type == :header
156
+ horizontal_rule(:char => options[:rule], :width => width, :color => color, :bold => bold) if options[:rule]
157
+ vertical_spacing(lines)
158
+ end
159
+ end
160
+
161
+ def assign_section_properties options
162
+ validate_options(options, :title, :width, :align, :spacing, :timestamp, :rule, :color, :bold)
163
+
164
+ title = options[:title] || 'Report'
165
+ width = options[:width] || DEFAULTS[:width]
166
+ align = options[:align] || DEFAULTS[:align]
167
+ lines = options[:spacing] || 1
168
+ color = options[:color]
169
+ bold = options[:bold] || false
170
+
171
+ return [title, width, align, lines, color, bold]
172
+ end
173
+ end
@@ -0,0 +1,416 @@
1
+ require 'spec_helper'
2
+
3
+ describe Columnist::Column do
4
+ describe '#initialize' do
5
+ it 'rejects invalid options' do
6
+ expect {
7
+ Columnist::Column.new('test', :asdf => '1234')
8
+ }.to raise_error ArgumentError
9
+ end
10
+
11
+ it 'defaults options hash' do
12
+ expect {
13
+ Columnist::Column.new('test')
14
+ }.to_not raise_error
15
+ end
16
+
17
+ it 'defaults the width' do
18
+ expect(Columnist::Column.new('test').width).to eq(10)
19
+ end
20
+
21
+ it 'accepts the width' do
22
+ expect(Columnist::Column.new('test', :width => 50).width).to eq(50)
23
+ end
24
+
25
+ it 'requires valid width' do
26
+ expect {
27
+ Columnist::Column.new('test', :width => 'asdf')
28
+ }.to raise_error ArgumentError
29
+ end
30
+
31
+ it 'accepts text' do
32
+ expect(Columnist::Column.new('asdf').text).to eq('asdf')
33
+ end
34
+
35
+ it 'accepts color' do
36
+ expect(Columnist::Column.new('asdf', :color => 'red').color).to eq('red')
37
+ end
38
+
39
+ it 'accepts bold' do
40
+ expect(Columnist::Column.new('asdf', :bold => true).bold).to be_true
41
+ end
42
+
43
+ it 'defaults the padding' do
44
+ expect(Columnist::Column.new('test').padding).to eq(0)
45
+ end
46
+
47
+ it 'accepts the padding' do
48
+ expect(Columnist::Column.new('test', :padding => 5).padding).to eq(5)
49
+ end
50
+
51
+ it 'requires valid width' do
52
+ expect {
53
+ Columnist::Column.new('test', :padding => 'asdf')
54
+ }.to raise_error ArgumentError
55
+ end
56
+ end
57
+
58
+ describe '#size' do
59
+ it 'is the width less twice the padding' do
60
+ expect(Columnist::Column.new('test').size).to eq(10)
61
+ expect(Columnist::Column.new('test', :width => 5).size).to eq(5)
62
+ expect(Columnist::Column.new('test', :width => 5, :padding => 1).size).to eq(3)
63
+ end
64
+ end
65
+
66
+ describe '#required_width' do
67
+ it 'is the length of the text plus twice the padding' do
68
+ expect(Columnist::Column.new('test').required_width).to eq(4)
69
+ expect(Columnist::Column.new('test', :padding => 1).required_width).to eq(6)
70
+ expect(Columnist::Column.new('test', :padding => 5).required_width).to eq(14)
71
+ end
72
+ end
73
+
74
+ describe '#screen_rows' do
75
+ let :controls do
76
+ {
77
+ :clear => "\e[0m",
78
+ :bold => "\e[1m",
79
+ :red => "\e[31m",
80
+ }
81
+ end
82
+
83
+ context 'no wrapping' do
84
+ context 'no padding' do
85
+ it 'gives a single row' do
86
+ c = Columnist::Column.new('x' * 5)
87
+ c.screen_rows.size == 1
88
+ end
89
+
90
+ it 'handles empty text' do
91
+ c = Columnist::Column.new
92
+ expect(c.screen_rows[0]).to eq(' ' * 10)
93
+ end
94
+
95
+ context 'left justifies' do
96
+ let(:text) { 'x' * 10 }
97
+ let(:filler) { ' ' * 10 }
98
+
99
+ it 'plain text' do
100
+ c = Columnist::Column.new(text, :width => 20)
101
+ expect(c.screen_rows[0]).to eq(text + filler)
102
+ end
103
+
104
+ it 'outputs red' do
105
+ c = Columnist::Column.new(text, :align => 'left', :width => 20, :color => 'red')
106
+ expect(c.screen_rows[0]).to eq(controls[:red] + text + filler + controls[:clear])
107
+ end
108
+
109
+ it 'outputs bold' do
110
+ c = Columnist::Column.new(text, :align => 'left', :width => 20, :bold => true)
111
+ expect(c.screen_rows[0]).to eq(controls[:bold] + text + filler + controls[:clear])
112
+ end
113
+ end
114
+
115
+ context 'right justifies' do
116
+ let(:text) { 'x' * 10 }
117
+ let(:filler) { ' ' * 10 }
118
+
119
+ it 'plain text' do
120
+ c = Columnist::Column.new(text, :align => 'right', :width => 20)
121
+ expect(c.screen_rows[0]).to eq(filler + text)
122
+ end
123
+
124
+ it 'outputs red' do
125
+ c = Columnist::Column.new(text, :align => 'right', :width => 20, :color => 'red')
126
+ expect(c.screen_rows[0]).to eq(controls[:red] + filler + text + controls[:clear])
127
+ end
128
+
129
+ it 'outputs bold' do
130
+ c = Columnist::Column.new(text, :align => 'right', :width => 20, :bold => true)
131
+ expect(c.screen_rows[0]).to eq(controls[:bold] + filler + text + controls[:clear])
132
+ end
133
+ end
134
+
135
+ context 'center justifies' do
136
+ let(:text) { 'x' * 10 }
137
+ let(:filler) { ' ' * 5 }
138
+
139
+ it 'plain text' do
140
+ c = Columnist::Column.new(text, :align => 'center', :width => 20)
141
+ expect(c.screen_rows[0]).to eq(filler + text + filler)
142
+ end
143
+
144
+ it 'outputs red' do
145
+ c = Columnist::Column.new(text, :align => 'center', :width => 20, :color => 'red')
146
+ expect(c.screen_rows[0]).to eq(controls[:red] + filler + text + filler + controls[:clear])
147
+ end
148
+
149
+ it 'outputs bold' do
150
+ c = Columnist::Column.new(text, :align => 'center', :width => 20, :bold => true)
151
+ expect(c.screen_rows[0]).to eq(controls[:bold] + filler + text + filler + controls[:clear])
152
+ end
153
+ end
154
+ end
155
+
156
+ context 'accounts for padding' do
157
+ context 'left justifies' do
158
+ let(:text) { 'x' * 10 }
159
+ let(:padding) { ' ' * 5 }
160
+ let(:filler) { ' ' * 10 }
161
+
162
+ it 'plain text' do
163
+ c = Columnist::Column.new(text, :padding => 5, :width => 30)
164
+ expect(c.screen_rows[0]).to eq(padding + text + filler + padding)
165
+ end
166
+
167
+ it 'outputs red' do
168
+ c = Columnist::Column.new(text, :padding => 5, :width => 30, :color => 'red')
169
+ expect(c.screen_rows[0]).to eq(padding + controls[:red] + text + filler + controls[:clear] + padding)
170
+ end
171
+
172
+ it 'outputs bold' do
173
+ c = Columnist::Column.new(text, :padding => 5, :width => 30, :bold => true)
174
+ expect(c.screen_rows[0]).to eq(padding + controls[:bold] + text + filler + controls[:clear] + padding)
175
+ end
176
+ end
177
+
178
+ context 'right justifies' do
179
+ let(:text) { 'x' * 10 }
180
+ let(:padding) { ' ' * 5 }
181
+ let(:filler) { ' ' * 10 }
182
+
183
+ it 'plain text' do
184
+ c = Columnist::Column.new(text, :align => 'right', :padding => 5, :width => 30)
185
+ expect(c.screen_rows[0]).to eq(padding + filler + text + padding)
186
+ end
187
+
188
+ it 'outputs red' do
189
+ c = Columnist::Column.new(text, :align => 'right', :padding => 5, :width => 30, :color => 'red')
190
+ expect(c.screen_rows[0]).to eq(padding + controls[:red] + filler + text + controls[:clear] + padding)
191
+ end
192
+
193
+ it 'outputs bold' do
194
+ c = Columnist::Column.new(text, :align => 'right', :padding => 5, :width => 30, :bold => true)
195
+ expect(c.screen_rows[0]).to eq(padding + controls[:bold] + filler + text + controls[:clear] + padding)
196
+ end
197
+ end
198
+
199
+ context 'right justifies' do
200
+ let(:text) { 'x' * 10 }
201
+ let(:padding) { ' ' * 5 }
202
+ let(:filler) { ' ' * 5 }
203
+
204
+ it 'plain text' do
205
+ c = Columnist::Column.new(text, :align => 'center', :padding => 5, :width => 30)
206
+ expect(c.screen_rows[0]).to eq(padding + filler + text + filler + padding)
207
+ end
208
+
209
+ it 'outputs red' do
210
+ c = Columnist::Column.new(text, :align => 'center', :padding => 5, :width => 30, :color => 'red')
211
+ expect(c.screen_rows[0]).to eq(padding + controls[:red] + filler + text + filler + controls[:clear] + padding)
212
+ end
213
+
214
+ it 'outputs bold' do
215
+ c = Columnist::Column.new(text, :align => 'center', :padding => 5, :width => 30, :bold => true)
216
+ expect(c.screen_rows[0]).to eq(padding + controls[:bold] + filler + text + filler + controls[:clear] + padding)
217
+ end
218
+ end
219
+ end
220
+ end
221
+
222
+ context 'with wrapping' do
223
+ context 'no padding' do
224
+ context 'left justifies' do
225
+ let(:text) { 'x' * 25 }
226
+ let(:full_line) { 'x' * 10 }
227
+ let(:remainder) { 'x' * 5 }
228
+ let(:filler) { ' ' * 5 }
229
+
230
+ it 'plain text' do
231
+ c = Columnist::Column.new(text, :width => 10)
232
+ expect(c.screen_rows).to eq([full_line, full_line, remainder + filler])
233
+ end
234
+
235
+ it 'outputs red' do
236
+ c = Columnist::Column.new(text, :width => 10, :color => 'red')
237
+ expect(c.screen_rows).to eq([
238
+ controls[:red] + full_line + controls[:clear],
239
+ controls[:red] + full_line + controls[:clear],
240
+ controls[:red] + remainder + filler + controls[:clear],
241
+ ])
242
+ end
243
+
244
+ it 'outputs bold' do
245
+ c = Columnist::Column.new(text, :width => 10, :bold => true)
246
+ expect(c.screen_rows).to eq([
247
+ controls[:bold] + full_line + controls[:clear],
248
+ controls[:bold] + full_line + controls[:clear],
249
+ controls[:bold] + remainder + filler + controls[:clear],
250
+ ])
251
+ end
252
+ end
253
+
254
+ context 'right justifies' do
255
+ let(:text) { 'x' * 25 }
256
+ let(:full_line) { 'x' * 10 }
257
+ let(:remainder) { 'x' * 5 }
258
+ let(:filler) { ' ' * 5 }
259
+
260
+ it 'plain text' do
261
+ c = Columnist::Column.new(text, :align => 'right', :width => 10)
262
+ expect(c.screen_rows).to eq([full_line, full_line, filler + remainder])
263
+ end
264
+
265
+ it 'outputs red' do
266
+ c = Columnist::Column.new(text, :align => 'right', :width => 10, :color => 'red')
267
+ expect(c.screen_rows).to eq([
268
+ controls[:red] + full_line + controls[:clear],
269
+ controls[:red] + full_line + controls[:clear],
270
+ controls[:red] + filler + remainder + controls[:clear],
271
+ ])
272
+ end
273
+
274
+ it 'outputs bold' do
275
+ c = Columnist::Column.new(text, :align => 'right', :width => 10, :bold => true)
276
+ expect(c.screen_rows).to eq([
277
+ controls[:bold] + full_line + controls[:clear],
278
+ controls[:bold] + full_line + controls[:clear],
279
+ controls[:bold] + filler + remainder + controls[:clear],
280
+ ])
281
+ end
282
+ end
283
+
284
+ context 'center justifies' do
285
+ let(:text) { 'x' * 25 }
286
+ let(:full_line) { 'x' * 10 }
287
+ let(:remainder) { 'x' * 5 }
288
+ let(:left_filler) { ' ' * 3 }
289
+ let(:right_filler) { ' ' * 2 }
290
+
291
+ it 'plain text' do
292
+ c = Columnist::Column.new(text, :align => 'center', :width => 10)
293
+ expect(c.screen_rows).to eq([full_line, full_line, ' ' * 3 + remainder + right_filler])
294
+ end
295
+
296
+ it 'outputs red' do
297
+ c = Columnist::Column.new(text, :align => 'center', :width => 10, :color => 'red')
298
+ expect(c.screen_rows).to eq([
299
+ controls[:red] + full_line + controls[:clear],
300
+ controls[:red] + full_line + controls[:clear],
301
+ controls[:red] + left_filler + remainder + right_filler + controls[:clear],
302
+ ])
303
+ end
304
+
305
+ it 'outputs bold' do
306
+ c = Columnist::Column.new(text, :align => 'center', :width => 10, :bold => true)
307
+ expect(c.screen_rows).to eq([
308
+ controls[:bold] + full_line + controls[:clear],
309
+ controls[:bold] + full_line + controls[:clear],
310
+ controls[:bold] + left_filler + remainder + right_filler + controls[:clear],
311
+ ])
312
+ end
313
+ end
314
+ end
315
+
316
+ context 'account for padding' do
317
+ context 'left justifies' do
318
+ let(:text) { 'x' * 25 }
319
+ let(:full_line) { 'x' * 16 }
320
+ let(:remainder) { 'x' * 9 }
321
+ let(:padding) { ' ' * 2 }
322
+ let(:filler) { ' ' * 7 }
323
+
324
+ it 'plain text' do
325
+ c = Columnist::Column.new(text, :padding => 2, :width => 20)
326
+ expect(c.screen_rows).to eq([
327
+ padding + full_line + padding,
328
+ padding + remainder + filler + padding,
329
+ ])
330
+ end
331
+
332
+ it 'outputs red' do
333
+ c = Columnist::Column.new(text, :padding => 2, :width => 20, :color => 'red')
334
+ expect(c.screen_rows).to eq([
335
+ padding + controls[:red] + full_line + controls[:clear] + padding,
336
+ padding + controls[:red] + remainder + filler + controls[:clear] + padding,
337
+ ])
338
+ end
339
+
340
+ it 'outputs bold' do
341
+ c = Columnist::Column.new(text, :padding => 2, :width => 20, :bold => true)
342
+ expect(c.screen_rows).to eq([
343
+ padding + controls[:bold] + full_line + controls[:clear] + padding,
344
+ padding + controls[:bold] + remainder + filler + controls[:clear] + padding,
345
+ ])
346
+ end
347
+ end
348
+
349
+ context 'right justifies' do
350
+ let(:text) { 'x' * 25 }
351
+ let(:full_line) { 'x' * 16 }
352
+ let(:remainder) { 'x' * 9 }
353
+ let(:padding) { ' ' * 2 }
354
+ let(:filler) { ' ' * 7 }
355
+
356
+ it 'plain text' do
357
+ c = Columnist::Column.new(text, :padding => 2, :align => 'right', :width => 20)
358
+ expect(c.screen_rows).to eq([
359
+ padding + full_line + padding,
360
+ padding + filler + remainder + padding,
361
+ ])
362
+ end
363
+
364
+ it 'outputs red' do
365
+ c = Columnist::Column.new(text, :align => 'right', :padding => 2, :width => 20, :color => 'red')
366
+ expect(c.screen_rows).to eq([
367
+ padding + controls[:red] + full_line + controls[:clear] + padding,
368
+ padding + controls[:red] + filler + remainder + controls[:clear] + padding,
369
+ ])
370
+ end
371
+
372
+ it 'outputs bold' do
373
+ c = Columnist::Column.new(text, :align => 'right', :padding => 2, :width => 20, :bold => true)
374
+ expect(c.screen_rows).to eq([
375
+ padding + controls[:bold] + full_line + controls[:clear] + padding,
376
+ padding + controls[:bold] + filler + remainder + controls[:clear] + padding,
377
+ ])
378
+ end
379
+ end
380
+
381
+ context 'center justifies' do
382
+ let(:text) { 'x' * 25 }
383
+ let(:full_line) { 'x' * 16 }
384
+ let(:remainder) { 'x' * 9 }
385
+ let(:padding) { ' ' * 2 }
386
+ let(:left_filler) { ' ' * 4 }
387
+ let(:right_filler) { ' ' * 3 }
388
+
389
+ it 'plain text' do
390
+ c = Columnist::Column.new(text, :padding => 2, :align => 'center', :width => 20)
391
+ expect(c.screen_rows).to eq([
392
+ padding + full_line + padding,
393
+ padding + left_filler + remainder + right_filler + padding,
394
+ ])
395
+ end
396
+
397
+ it 'outputs red' do
398
+ c = Columnist::Column.new(text, :padding => 2, :align => 'center', :width => 20, :color => 'red')
399
+ expect(c.screen_rows).to eq([
400
+ padding + controls[:red] + full_line + controls[:clear] + padding,
401
+ padding + controls[:red] + left_filler + remainder + right_filler + controls[:clear] + padding,
402
+ ])
403
+ end
404
+
405
+ it 'outputs bold' do
406
+ c = Columnist::Column.new(text, :padding => 2, :align => 'center', :width => 20, :bold => true)
407
+ expect(c.screen_rows).to eq([
408
+ padding + controls[:bold] + full_line + controls[:clear] + padding,
409
+ padding + controls[:bold] + left_filler + remainder + right_filler + controls[:clear] + padding,
410
+ ])
411
+ end
412
+ end
413
+ end
414
+ end
415
+ end
416
+ end