tty 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +7 -3
- data/Rakefile +2 -0
- data/lib/tty/support/equatable.rb +151 -0
- data/lib/tty/table/operation/alignment.rb +40 -13
- data/lib/tty/table/operation/alignment_set.rb +17 -6
- data/lib/tty/table/operation/truncation.rb +42 -0
- data/lib/tty/table/renderer/basic.rb +53 -32
- data/lib/tty/table/renderer.rb +17 -0
- data/lib/tty/table/validatable.rb +0 -3
- data/lib/tty/table.rb +30 -36
- data/lib/tty/version.rb +1 -1
- data/lib/tty.rb +2 -0
- data/spec/tty/support/equatable_spec.rb +206 -0
- data/spec/tty/table/access_spec.rb +8 -1
- data/spec/tty/table/eql_spec.rb +7 -1
- data/spec/tty/table/initialize_spec.rb +45 -37
- data/spec/tty/table/operation/alignment/format_spec.rb +13 -5
- data/spec/tty/table/operation/alignment_set/align_rows_spec.rb +4 -4
- data/spec/tty/table/operation/truncation/truncate_spec.rb +22 -0
- data/spec/tty/table/options_spec.rb +8 -2
- data/spec/tty/table/renderer/basic/alignment_spec.rb +40 -0
- data/spec/tty/table/renderer/basic/extract_column_widths_spec.rb +23 -0
- data/spec/tty/table/renderer/basic/new_spec.rb +11 -0
- data/spec/tty/table/renderer/basic/render_spec.rb +48 -0
- data/spec/tty/table/renderer/basic_spec.rb +0 -64
- data/spec/tty/table/renderer_spec.rb +2 -2
- data/tasks/metrics/cane.rake +12 -0
- data/tasks/metrics/flog.rake +15 -0
- data/tasks/metrics/reek.rake +13 -0
- metadata +25 -8
data/README.md
CHANGED
@@ -38,15 +38,18 @@ Or install it yourself as:
|
|
38
38
|
|
39
39
|
### Table
|
40
40
|
|
41
|
-
To instantiate table
|
41
|
+
To instantiate table pass 2-dimensional array:
|
42
42
|
|
43
43
|
```ruby
|
44
44
|
table = TTY::Table[['a1', 'a2'], ['b1', 'b2']]
|
45
45
|
table = TTY::Table.new [['a1', 'a2'], ['b1', 'b2']]
|
46
46
|
table = TTY::Table.new rows: [['a1', 'a2'], ['b1', 'b2']]
|
47
|
+
|
48
|
+
table = TTY::Table.new ['h1', 'h2'], [['a1', 'a2'], ['b1', 'b2']]
|
49
|
+
table = TTY::Table.new header: ['h1', 'h2'], rows: [['a1', 'a2'], ['b1', 'b2']]
|
47
50
|
```
|
48
51
|
|
49
|
-
Apart from `rows`, you can provide other customization options such as
|
52
|
+
Apart from `rows` and `header`, you can provide other customization options such as
|
50
53
|
|
51
54
|
```ruby
|
52
55
|
column_widths # enforce maximum columns widths
|
@@ -59,6 +62,7 @@ Table behaves like an Array so `<<`, `each` and familiar methods can be used
|
|
59
62
|
```ruby
|
60
63
|
table << ['a1', 'a2', 'a3']
|
61
64
|
table << ['b1', 'b2', 'b3']
|
65
|
+
table << ['a1', 'a2'] << ['b1', 'b2'] # chain rows assignment
|
62
66
|
|
63
67
|
table.each { |row| ... } # iterate over rows
|
64
68
|
table[i, j] # return element at row(i) and column(j)
|
@@ -66,6 +70,7 @@ Table behaves like an Array so `<<`, `each` and familiar methods can be used
|
|
66
70
|
table.column(j) { ... } # return array for column(j)
|
67
71
|
table.row_size # return row size
|
68
72
|
table.column_size # return column size
|
73
|
+
table.size # return an array of [row_size, column_size]
|
69
74
|
```
|
70
75
|
|
71
76
|
or pass your rows in a block
|
@@ -77,7 +82,6 @@ or pass your rows in a block
|
|
77
82
|
end
|
78
83
|
```
|
79
84
|
|
80
|
-
|
81
85
|
And then to print do
|
82
86
|
|
83
87
|
```ruby
|
data/Rakefile
CHANGED
@@ -0,0 +1,151 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
|
5
|
+
# Make it easy to define equality and hash methods.
|
6
|
+
module Equatable
|
7
|
+
|
8
|
+
# Hook into module inclusion.
|
9
|
+
#
|
10
|
+
# @param [Module] base
|
11
|
+
#
|
12
|
+
# @return [self]
|
13
|
+
#
|
14
|
+
# @api private
|
15
|
+
def self.included(base)
|
16
|
+
super
|
17
|
+
base.extend(self)
|
18
|
+
base.class_eval do
|
19
|
+
define_comparison_attrs
|
20
|
+
include Methods
|
21
|
+
define_methods
|
22
|
+
end
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
# Holds all attributes used for comparison.
|
27
|
+
#
|
28
|
+
# @return [Array<Symbol>]
|
29
|
+
#
|
30
|
+
# @api private
|
31
|
+
attr_reader :comparison_attrs
|
32
|
+
|
33
|
+
# Objects that include this module are assumed to be value objects.
|
34
|
+
# It is also assumed that the only values that affect the results of
|
35
|
+
# equality comparison are the values of the object's attributes.
|
36
|
+
#
|
37
|
+
# @param [Array<Symbol>] *args
|
38
|
+
#
|
39
|
+
# @return [undefined]
|
40
|
+
#
|
41
|
+
# @api public
|
42
|
+
def attr_reader(*args)
|
43
|
+
super
|
44
|
+
@comparison_attrs.concat(args)
|
45
|
+
end
|
46
|
+
|
47
|
+
# Copy the comparison_attrs into the subclass.
|
48
|
+
#
|
49
|
+
# @param [Class] subclass
|
50
|
+
#
|
51
|
+
# @api private
|
52
|
+
def inherited(subclass)
|
53
|
+
super
|
54
|
+
subclass.instance_variable_set(:@comparison_attrs, comparison_attrs.dup)
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
# Define class instance #comparison_attrs as an empty array.
|
60
|
+
#
|
61
|
+
# @return [undefined]
|
62
|
+
#
|
63
|
+
# @api private
|
64
|
+
def define_comparison_attrs
|
65
|
+
instance_variable_set('@comparison_attrs', [])
|
66
|
+
end
|
67
|
+
|
68
|
+
# Define all methods needed for ensuring object's equality.
|
69
|
+
#
|
70
|
+
# @return [undefined]
|
71
|
+
#
|
72
|
+
# @api private
|
73
|
+
def define_methods
|
74
|
+
define_compare
|
75
|
+
define_hash
|
76
|
+
define_inspect
|
77
|
+
end
|
78
|
+
|
79
|
+
# Define a #compare? method to check if the receiver is the same
|
80
|
+
# as the other object.
|
81
|
+
#
|
82
|
+
# @return [undefined]
|
83
|
+
#
|
84
|
+
# @api private
|
85
|
+
def define_compare
|
86
|
+
define_method(:compare?) do |comparator, other|
|
87
|
+
attrs = comparison_attrs || []
|
88
|
+
!attrs.find { |attr| send(attr).send(comparator, other.send(attr)) }
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Define a hash method that ensures that the hash value is the same for
|
93
|
+
# the same instance attributes and their corresponding values.
|
94
|
+
#
|
95
|
+
# @api private
|
96
|
+
def define_hash
|
97
|
+
define_method(:hash) do
|
98
|
+
klass = self.class
|
99
|
+
attrs = klass.comparison_attrs || []
|
100
|
+
([klass] + attrs.map { |attr| send(attr)}).hash
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
# Define an inspect method that shows the class name and the values for the
|
105
|
+
# instance's attributes.
|
106
|
+
#
|
107
|
+
# @return [undefined]
|
108
|
+
#
|
109
|
+
# @api private
|
110
|
+
def define_inspect
|
111
|
+
define_method(:inspect) do
|
112
|
+
klass = self.class
|
113
|
+
name = klass.name || klass.inspect
|
114
|
+
attrs = klass.comparison_attrs || []
|
115
|
+
"#<#{name}#{attrs.map { |attr| " #{attr}=#{send(attr).inspect}" }.join}>"
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
module Methods
|
120
|
+
|
121
|
+
# Compare two objects for equality based on their value
|
122
|
+
# and being an instance of the given class.
|
123
|
+
#
|
124
|
+
# @param [Object] other
|
125
|
+
# the other object in comparison
|
126
|
+
#
|
127
|
+
# @return [Boolean]
|
128
|
+
#
|
129
|
+
# @api public
|
130
|
+
def eql?(other)
|
131
|
+
instance_of?(other.class) and compare?(__method__, other)
|
132
|
+
end
|
133
|
+
|
134
|
+
# Compare two objects for equality based on their value
|
135
|
+
# and being a subclass of the given class.
|
136
|
+
#
|
137
|
+
# @param [Object] other
|
138
|
+
# the other object in comparison
|
139
|
+
#
|
140
|
+
# @return [Boolean]
|
141
|
+
#
|
142
|
+
# @api public
|
143
|
+
def ==(other)
|
144
|
+
return false unless self.class <=> other.class
|
145
|
+
compare?(__method__, other)
|
146
|
+
end
|
147
|
+
|
148
|
+
end # Methods
|
149
|
+
|
150
|
+
end # Equatable
|
151
|
+
end # TTY
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
1
3
|
module TTY
|
2
4
|
class Table
|
3
5
|
module Operation
|
@@ -10,6 +12,9 @@ module TTY
|
|
10
12
|
|
11
13
|
CENTER = :center.freeze
|
12
14
|
|
15
|
+
# Hold the type of alignment
|
16
|
+
#
|
17
|
+
# @api private
|
13
18
|
attr_reader :type
|
14
19
|
|
15
20
|
# Initialize an Alignment
|
@@ -25,7 +30,6 @@ module TTY
|
|
25
30
|
assert_valid_type
|
26
31
|
end
|
27
32
|
|
28
|
-
|
29
33
|
# Assert the type is valid
|
30
34
|
#
|
31
35
|
# @return [undefined]
|
@@ -50,26 +54,49 @@ module TTY
|
|
50
54
|
[LEFT, RIGHT, CENTER]
|
51
55
|
end
|
52
56
|
|
53
|
-
# Format
|
57
|
+
# Format field with a given alignment
|
58
|
+
#
|
59
|
+
# @param [Object] field
|
60
|
+
#
|
61
|
+
# @param [Integer] column_width
|
62
|
+
#
|
63
|
+
# @param [String] space
|
54
64
|
#
|
55
65
|
# @return [String] aligned
|
56
66
|
#
|
57
67
|
# @api public
|
58
|
-
def format(
|
68
|
+
def format(field, column_width, space='')
|
59
69
|
case type
|
60
70
|
when :left
|
61
|
-
"%-#{column_width}s#{space}" %
|
71
|
+
"%-#{column_width}s#{space}" % field.to_s
|
62
72
|
when :right
|
63
|
-
"%#{column_width}s#{space}" %
|
73
|
+
"%#{column_width}s#{space}" % field.to_s
|
64
74
|
when :center
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
75
|
+
center_align field, column_width, space
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
# Center aligns field
|
82
|
+
#
|
83
|
+
# @param [Object] field
|
84
|
+
#
|
85
|
+
# @param [Integer] column_width
|
86
|
+
#
|
87
|
+
# @param [String] space
|
88
|
+
#
|
89
|
+
# @return [String] aligned
|
90
|
+
#
|
91
|
+
# @api private
|
92
|
+
def center_align(field, column_width, space)
|
93
|
+
chars = field.to_s.chars.to_a
|
94
|
+
if column_width >= chars.size
|
95
|
+
right = ((pad_length = column_width - chars.length).to_f / 2).ceil
|
96
|
+
left = pad_length - right
|
97
|
+
[' ' * left, field, ' ' * right, space].join
|
98
|
+
else
|
99
|
+
"#{field}#{space}"
|
73
100
|
end
|
74
101
|
end
|
75
102
|
|
@@ -3,6 +3,8 @@
|
|
3
3
|
module TTY
|
4
4
|
class Table
|
5
5
|
module Operation
|
6
|
+
|
7
|
+
# A class which responsiblity is to align table rows and header.
|
6
8
|
class AlignmentSet
|
7
9
|
include Enumerable
|
8
10
|
|
@@ -61,7 +63,7 @@ module TTY
|
|
61
63
|
@alignments
|
62
64
|
end
|
63
65
|
|
64
|
-
# @return [Array]
|
66
|
+
# @return [Array[Alignment]]
|
65
67
|
#
|
66
68
|
# @api public
|
67
69
|
def to_a
|
@@ -75,11 +77,20 @@ module TTY
|
|
75
77
|
to_ary.empty?
|
76
78
|
end
|
77
79
|
|
78
|
-
#
|
80
|
+
# Aligns table header
|
81
|
+
#
|
82
|
+
# @return [Array[String]]
|
83
|
+
#
|
84
|
+
# @api public
|
85
|
+
def align_header(header, options={})
|
86
|
+
align_row(header, options)
|
87
|
+
end
|
88
|
+
|
89
|
+
# Align the supplied rows with the correct alignment.
|
79
90
|
#
|
80
91
|
# @param [Array] rows
|
81
92
|
#
|
82
|
-
# @return [Array]
|
93
|
+
# @return [Array[Array]]
|
83
94
|
# the aligned rows
|
84
95
|
#
|
85
96
|
# @api private
|
@@ -95,17 +106,17 @@ module TTY
|
|
95
106
|
#
|
96
107
|
# @param [Hash] options
|
97
108
|
#
|
98
|
-
# @return [String]
|
109
|
+
# @return [Array[String]]
|
99
110
|
#
|
100
111
|
# @api private
|
101
112
|
def align_row(row, options={})
|
102
|
-
line =
|
113
|
+
line = []
|
103
114
|
row.each_with_index do |cell, index|
|
104
115
|
column_width = options[:column_widths][index]
|
105
116
|
alignment = Alignment.new self[index]
|
106
117
|
|
107
118
|
if index == row.size - 1
|
108
|
-
line <<
|
119
|
+
line << alignment.format(cell, column_width)
|
109
120
|
else
|
110
121
|
line << alignment.format(cell, column_width, ' ')
|
111
122
|
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module TTY
|
4
|
+
class Table
|
5
|
+
module Operation
|
6
|
+
|
7
|
+
# A class responsible for shortening text.
|
8
|
+
class Truncation
|
9
|
+
|
10
|
+
def truncate(string, width, options={})
|
11
|
+
trailing = options.fetch :trailing, '…'
|
12
|
+
|
13
|
+
as_unicode do
|
14
|
+
chars = string.chars.to_a
|
15
|
+
print 'CHARS '
|
16
|
+
p chars
|
17
|
+
if chars.length < width
|
18
|
+
chars.join
|
19
|
+
else
|
20
|
+
traling_size = trailing.chars.to_a.size
|
21
|
+
( chars[0, width-traling_size].join ) + trailing
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
if "".respond_to?(:encode)
|
27
|
+
def as_unicode
|
28
|
+
yield
|
29
|
+
end
|
30
|
+
else
|
31
|
+
def as_unicode
|
32
|
+
old, $KCODE = $KCODE, "U"
|
33
|
+
yield
|
34
|
+
ensure
|
35
|
+
$KCODE = old
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
end # Truncation
|
40
|
+
end # Operation
|
41
|
+
end # Table
|
42
|
+
end # TTY
|
@@ -3,23 +3,25 @@
|
|
3
3
|
module TTY
|
4
4
|
class Table
|
5
5
|
module Renderer
|
6
|
+
|
7
|
+
# Renders table without any border styles.
|
6
8
|
class Basic
|
9
|
+
extend TTY::Delegatable
|
7
10
|
|
8
11
|
attr_reader :padding
|
9
12
|
|
10
13
|
attr_reader :indent
|
11
14
|
|
12
|
-
|
13
|
-
|
14
|
-
attr_reader :column_aligns
|
15
|
-
|
16
|
-
# Return an array of table rows
|
15
|
+
# Table to be rendered
|
17
16
|
#
|
18
|
-
# @return [
|
17
|
+
# @return [TTY::Table]
|
19
18
|
#
|
20
|
-
# @api
|
21
|
-
attr_reader :
|
22
|
-
|
19
|
+
# @api public
|
20
|
+
attr_reader :table
|
21
|
+
|
22
|
+
TABLE_DELEGATED_METHODS = [:column_widths, :alignments]
|
23
|
+
|
24
|
+
delegatable_method :table, *TABLE_DELEGATED_METHODS
|
23
25
|
|
24
26
|
# Return an AlignmentSet object for processing alignments
|
25
27
|
#
|
@@ -34,7 +36,6 @@ module TTY
|
|
34
36
|
# @param [Hash] options
|
35
37
|
# :indent - Indent the first column by indent value
|
36
38
|
# :padding - Pad out the row cell by padding value
|
37
|
-
# :col_widths - Enforce particular column width values
|
38
39
|
#
|
39
40
|
# @return [Table::Renderer::Basic]
|
40
41
|
def initialize(options={})
|
@@ -49,10 +50,6 @@ module TTY
|
|
49
50
|
def setup(options = {})
|
50
51
|
@padding = 0
|
51
52
|
@indent = options.fetch :indent, 0
|
52
|
-
# TODO: assert that row_size is the same as column widths & aligns
|
53
|
-
@column_widths = options.fetch :column_widths, []
|
54
|
-
@column_aligns = options.fetch :column_aligns, []
|
55
|
-
@alignment_set = TTY::Table::Operation::AlignmentSet.new column_aligns
|
56
53
|
self
|
57
54
|
end
|
58
55
|
private :setup
|
@@ -68,29 +65,33 @@ module TTY
|
|
68
65
|
end
|
69
66
|
|
70
67
|
# @api public
|
71
|
-
def self.render(
|
72
|
-
new(options).render(
|
68
|
+
def self.render(table, options={})
|
69
|
+
new(options).render(table)
|
73
70
|
end
|
74
71
|
|
75
72
|
# Renders table
|
76
73
|
#
|
77
|
-
# @param [
|
78
|
-
# the table
|
74
|
+
# @param [TTY::Table] table
|
75
|
+
# the table to be rendered
|
79
76
|
#
|
80
77
|
# @return [String] string representation of table
|
81
78
|
#
|
82
79
|
# @api public
|
83
|
-
def render(
|
84
|
-
|
85
|
-
|
86
|
-
#
|
87
|
-
@rows = rows
|
80
|
+
def render(table)
|
81
|
+
@table = table
|
82
|
+
return if table.to_a.empty?
|
83
|
+
# setup(options)
|
88
84
|
body = []
|
89
|
-
unless
|
90
|
-
extract_column_widths
|
85
|
+
unless table.length.zero?
|
86
|
+
extract_column_widths
|
87
|
+
# TODO: throw an error if too many columns as compared to terminal width
|
88
|
+
# and then change table.orientation from vertical to horizontal
|
89
|
+
# TODO: Decide about table orientation
|
90
|
+
|
91
|
+
body += render_header
|
91
92
|
body += render_rows
|
92
93
|
end
|
93
|
-
body.join("\n")
|
94
|
+
body.compact.join("\n")
|
94
95
|
end
|
95
96
|
|
96
97
|
# Calcualte total table width
|
@@ -102,25 +103,43 @@ module TTY
|
|
102
103
|
column_widths.reduce(:+)
|
103
104
|
end
|
104
105
|
|
106
|
+
private
|
107
|
+
|
105
108
|
# Calcualte maximum column widths
|
106
109
|
#
|
107
110
|
# @return [Array] column widths
|
108
111
|
#
|
109
112
|
# @api private
|
110
|
-
def extract_column_widths
|
113
|
+
def extract_column_widths
|
111
114
|
return column_widths unless column_widths.empty?
|
112
|
-
|
113
|
-
colcount =
|
115
|
+
data = table.header ? table.to_a + [table.header] : table.to_a
|
116
|
+
colcount = data.max{ |a,b| a.size <=> b.size }.size
|
114
117
|
maximas = []
|
115
118
|
start = 0
|
116
119
|
|
117
120
|
start.upto(colcount - 1) do |index|
|
118
|
-
maximum =
|
121
|
+
maximum = data.map { |row|
|
119
122
|
row[index] ? (row[index].to_s.size) : 0
|
120
123
|
}.max
|
121
124
|
maximas << maximum
|
122
125
|
end
|
123
|
-
|
126
|
+
table.column_widths = maximas
|
127
|
+
end
|
128
|
+
|
129
|
+
# Format the header
|
130
|
+
#
|
131
|
+
# @return [Array[String]]
|
132
|
+
#
|
133
|
+
# @api private
|
134
|
+
def render_header
|
135
|
+
header = table.header
|
136
|
+
if header
|
137
|
+
aligned = alignments.align_header header,
|
138
|
+
:column_widths => column_widths
|
139
|
+
[aligned.join]
|
140
|
+
else
|
141
|
+
[]
|
142
|
+
end
|
124
143
|
end
|
125
144
|
|
126
145
|
# Format the rows
|
@@ -129,7 +148,9 @@ module TTY
|
|
129
148
|
#
|
130
149
|
# @api private
|
131
150
|
def render_rows
|
132
|
-
|
151
|
+
aligned = alignments.align_rows table.to_a,
|
152
|
+
:column_widths => column_widths
|
153
|
+
aligned.map { |row| row.join }
|
133
154
|
end
|
134
155
|
|
135
156
|
end # Basic
|
data/lib/tty/table/renderer.rb
CHANGED
@@ -3,6 +3,10 @@
|
|
3
3
|
module TTY
|
4
4
|
class Table
|
5
5
|
|
6
|
+
# Determine renderer based on terminal capabilities
|
7
|
+
#
|
8
|
+
# @return [TTY::Table::Renderer]
|
9
|
+
#
|
6
10
|
# @api public
|
7
11
|
def self.renderer
|
8
12
|
@renderer ||= if TTY.terminal.color?
|
@@ -45,6 +49,14 @@ module TTY
|
|
45
49
|
self.renderer = RENDERER_MAPPER[:"#{options[:renderer]}"].new
|
46
50
|
end
|
47
51
|
|
52
|
+
# Determine renderer class based on string name
|
53
|
+
#
|
54
|
+
# @param [TTY::Table::Renderer] renderer
|
55
|
+
# the renderer used for displaying table
|
56
|
+
#
|
57
|
+
# @return [TTY::Table::Renderer]
|
58
|
+
#
|
59
|
+
# @api private
|
48
60
|
def pick_renderer(renderer)
|
49
61
|
if renderer
|
50
62
|
RENDERER_MAPPER[renderer].new
|
@@ -62,6 +74,11 @@ module TTY
|
|
62
74
|
@renderer ||= TTY::Table.renderer.new
|
63
75
|
end
|
64
76
|
|
77
|
+
# Set the renderer
|
78
|
+
#
|
79
|
+
# @return [TTY::Table::Renderer]
|
80
|
+
#
|
81
|
+
# @api private
|
65
82
|
def renderer=(renderer)
|
66
83
|
@renderer = renderer
|
67
84
|
end
|