tty 0.0.3 → 0.0.4
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/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
|