text_layout 0.1.1 → 0.2.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.
@@ -6,3 +6,4 @@ Manifest.txt
6
6
  Rakefile
7
7
  README.rdoc
8
8
  test/test_text_layout.rb
9
+ test/test_wrap.rb
@@ -4,13 +4,14 @@
4
4
 
5
5
  == DESCRIPTION:
6
6
 
7
- Text layout engine for CUI apps.
7
+ Text layout engine for CUI apps with a pretty table.
8
8
 
9
9
  == FEATURES/PROBLEMS:
10
10
 
11
11
  * Full-width character support.
12
12
  * Line wrapping.
13
13
  * Table layout with colspan and rowspan.
14
+ * Table with border.
14
15
 
15
16
  == SYNOPSIS:
16
17
 
@@ -24,21 +25,27 @@ Text layout engine for CUI apps.
24
25
  [{:colspan => 2, :value => "37Wh(13\")"}, "40Wh(13\")", {:colspan => 2, :value => "50Wh(13\")"}]
25
26
  ]
26
27
 
27
- puts TextLayout::Table.new(table).layout
28
+ puts TextLayout::Table.new(table, :border => true).layout
28
29
 
29
30
  ---
30
31
 
31
- | Table of models |
32
+ +----------------------------------------------------------------------------------------------------+
33
+ | Table of models |
34
+ +------------------+---------------+-----------+-----------+--------------------+--------------------+
32
35
  | Model | Early 2008 | Late 2008 | Mid 2009 | Late 2010 | Mid 2011 |
36
+ +------------------+---------------+-----------+-----------+--------------------+--------------------+
33
37
  | Model Identifier | MacBookAir1,1 | MacBookAir2,1 | MacBookAir3,1(11") | MacBookAir4,1(11") |
34
38
  | | | | MacBookAir3,2(13") | MacBookAir4,2(13") |
39
+ +------------------+---------------+-----------+-----------+--------------------+--------------------+
35
40
  | Model number | MB003LL/A | MB543LL/A | MC233LL/A | MC505LL/A | MC968ZP/A |
36
41
  | | | MB940LL/A | MC234LL/A | MC503LL/A | MC969ZP/A |
37
42
  | | | | | | MC965ZP/A |
38
43
  | | | | | | MC966ZP/A |
39
- | Battery | N/A | 35Wh(11") |
44
+ +------------------+---------------+-----------+-----------+--------------------+--------------------+
45
+ | Battery | N/A | 35Wh(11") |
46
+ | +---------------------------+-----------+-----------------------------------------+
40
47
  | | 37Wh(13") | 40Wh(13") | 50Wh(13") |
41
-
48
+ +------------------+---------------------------+-----------+-----------------------------------------+
42
49
 
43
50
  == REQUIREMENTS:
44
51
 
@@ -48,6 +55,13 @@ Text layout engine for CUI apps.
48
55
 
49
56
  * $ gem install text_layout
50
57
 
58
+ == OTHER CHOICES
59
+
60
+ * https://github.com/aptinio/text-table
61
+ * https://github.com/jwulff/pretty_table
62
+ * https://github.com/visionmedia/terminal-table
63
+ * http://rubyworks.github.com/ansi/docs/api/ANSI/Table.html
64
+
51
65
  == DEVELOPERS:
52
66
 
53
67
  After checking out the source, run:
@@ -61,7 +75,7 @@ and generate the RDoc.
61
75
 
62
76
  (The MIT License)
63
77
 
64
- Copyright (c) 2011 FIX
78
+ Copyright (c) 2011 NANKI Haruo
65
79
 
66
80
  Permission is hereby granted, free of charge, to any person obtaining
67
81
  a copy of this software and associated documentation files (the
@@ -1,7 +1,7 @@
1
1
  require 'unicode/display_width'
2
2
 
3
3
  module TextLayout
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
 
6
6
  autoload :Wrap, 'text_layout/wrap'
7
7
  autoload :Table, 'text_layout/table'
@@ -1,27 +1,42 @@
1
1
  class TextLayout::Table
2
- def initialize(array)
3
- @array = array
4
- end
2
+ def initialize(table, options = {})
3
+ @table = table
4
+ @options = {
5
+ :align => :auto,
6
+ :col_border => "|",
7
+ :row_border => "-",
8
+ :cross => "+",
9
+ :border => false,
10
+ :padding => " "
11
+ }.merge(options)
5
12
 
6
- def normalize(cell)
7
- unless Hash === cell
8
- cell = {:value => cell}
13
+ if @options[:border] == true
14
+ @options[:border] = [:top, :bottom, :left, :right, :cell].inject({}){|r, i|r[i] = true;r}
9
15
  end
16
+ end
10
17
 
11
- cell[:value] = cell[:value].to_s.lines.map(&:strip)
12
- cell
18
+ def column_border_width
19
+ @column_border_width ||= (@options[:padding] + @options[:col_border] + @options[:padding]).display_width
20
+ end
21
+
22
+ def cell_format
23
+ @cell_format ||= @options[:padding] + "%s" + @options[:padding]
24
+ end
25
+
26
+ def line_format
27
+ @line_format ||= @options[:col_border] + "%s" + @options[:col_border]
13
28
  end
14
29
 
15
30
  class Cell < Struct.new(:col, :row, :attr)
16
31
  def width
17
- attr[:value].map(&:display_width).max
32
+ attr[:value].map(&:display_width).max || 0
18
33
  end
19
34
 
20
35
  def height
21
- attr[:value].size
36
+ [attr[:value].size, 1].max
22
37
  end
23
38
  end
24
-
39
+
25
40
  class Span < Cell
26
41
  def main?(col, row)
27
42
  self.col == col && self.row == row
@@ -29,110 +44,239 @@ class TextLayout::Table
29
44
  end
30
45
 
31
46
  def layout
32
- spanss = {:row => Hash.new{[]}, :col => Hash.new{[]}}
33
- layout = @array.map{[]}
47
+ unknot
48
+ calculate_cell_size
49
+ expand_cell_size
50
+ build_string.join("\n")
51
+ end
34
52
 
35
- @array.each_with_index do |cols, row|
53
+ private
54
+ def unknot
55
+ @unknotted = @table.map{[]}
56
+ @spanss = {:row => Hash.new{[]}, :col => Hash.new{[]}}
57
+ @border = {:row => [], :col => []}
58
+
59
+ @table.each_with_index do |cols, row|
36
60
  cols.each_with_index do |attr, col|
37
61
  attr = normalize(attr)
62
+ col = @unknotted[row].index(nil) || @unknotted[row].size
63
+ rowspan, colspan = attr[:rowspan], attr[:colspan]
38
64
 
39
- unless col = layout[row].index(nil)
40
- col = layout[row].size
41
- end
42
-
43
- if !attr[:rowspan] && !attr[:colspan]
44
- layout[row][col] = Cell.new(col, row, attr)
65
+ if !rowspan && !colspan
66
+ @unknotted[row][col] = Cell.new(col, row, attr)
45
67
  else
46
68
  span = Span.new(col, row, attr)
47
- spanss[:row][row...row+attr[:rowspan]] <<= span if attr[:rowspan]
48
- spanss[:col][col...col+attr[:colspan]] <<= span if attr[:colspan]
49
-
50
- (attr[:rowspan] || 1).times do |rr|
51
- (attr[:colspan] || 1).times do |cc|
52
- layout[row+rr][col+cc] = span
69
+ @spanss[:row][[row, rowspan]] <<= span if rowspan
70
+ @spanss[:col][[col, colspan]] <<= span if colspan
71
+
72
+ (rowspan || 1).times do |rr|
73
+ (colspan || 1).times do |cc|
74
+ @unknotted[row+rr][col+cc] = span
53
75
  end
54
76
  end
55
77
  end
78
+
79
+ @border[:row][row - 1] ||= @unknotted[row][col] != @unknotted[row - 1][col] if row > 0
80
+ @border[:col][col - 1] ||= @unknotted[row][col] != @unknotted[row][col - 1] if col > 0
56
81
  end
57
82
  end
58
83
 
59
- max = {:row => [], :col => []}
84
+ [:row, :col].each{|dir| @border[dir].map!{|v| v ? 1 : 0 } }
85
+ @num_rows = @unknotted.size
86
+ @num_cols = @unknotted.first.size
87
+ end
60
88
 
61
- layout.each_with_index do |cols, row|
89
+ def calculate_cell_size
90
+ @cell_size = {:row => [0] * @num_rows, :col => [0] * @num_cols}
91
+
92
+ @unknotted.each_with_index do |cols, row|
62
93
  cols.each_with_index do |cell, col|
63
94
  next if Span === cell && !cell.main?(col, row)
64
95
 
65
- unless cell.attr[:colspan]
66
- max[:col][col] = [max[:col][col] || 0, cell.width].max
67
- end
68
-
69
- unless cell.attr[:rowspan]
70
- max[:row][row] = [max[:row][row] || 0, cell.height].max
71
- end
96
+ @cell_size[:col][col] = [@cell_size[:col][col], cell.width ].max unless cell.attr[:colspan]
97
+ @cell_size[:row][row] = [@cell_size[:row][row], cell.height].max unless cell.attr[:rowspan]
72
98
  end
73
99
  end
100
+ end
74
101
 
75
- [[:col, 3], [:row, 0]].each do |dir, margin|
76
- spanss[dir].each do |range, spans|
77
- range_size = range.end - range.begin
102
+ def expand_cell_size
103
+ [[:col, column_border_width], [:row, @options[:border] ? 1 : 0]].each do |dir, margin|
104
+ @spanss[dir].each do |range, spans|
105
+ rstart, rsize = range
78
106
  spans.each do |span|
79
- size = sum(max[dir][range]) + margin * (range_size - 1)
107
+ borders = sum(@border[dir][rstart, rsize-1])
108
+ size = sum(@cell_size[dir][*range]) + margin * borders
80
109
  diff = (dir == :col ? span.width : span.height) - size
81
110
 
82
111
  next unless diff > 0
83
- q, r = diff.divmod range_size
84
- range.each_with_index do |i, rr|
85
- max[dir][i] += q + (rr < r ? 1 : 0)
112
+
113
+ q, r = diff.divmod rsize
114
+ rsize.times do |i|
115
+ @cell_size[dir][rstart + i] += q + (i < r ? 1 : 0)
86
116
  end
87
117
  end
88
118
  end
89
119
  end
120
+ end
90
121
 
91
- result = ""
92
- sum(max[:row]).times do |layout_row|
93
- n = layout_row
94
- row = max[:row].each_with_index do |height, i|
95
- if n - height >= 0
96
- n -= height
97
- else
98
- break i
99
- end
122
+ def build_string
123
+ lines = []
124
+
125
+ total_lines = sum(@cell_size[:row])
126
+
127
+ if @options[:border]
128
+ total_lines += sum(@border[:row])
129
+ lines << row_border(-1) if @options[:border][:top]
130
+ end
131
+
132
+ total_lines.times do |display_row|
133
+ n = display_row
134
+ row, on_border = @cell_size[:row].each_with_index do |height, i|
135
+ n -= height
136
+ break i, false if n < 0
137
+
138
+ next unless @options[:border] && @options[:border][:cell]
139
+
140
+ n -= @border[:row][i] || 1
141
+ break i, true if n < 0
100
142
  end
101
143
 
102
144
  line = []
103
- layout[row].each_with_index do |cell, col|
104
- next unless cell.col == col
105
- n = layout_row - sum(max[:row][0...cell.row])
106
- width =
107
- if Span === cell && colspan = cell.attr[:colspan]
108
- sum(max[:col][col...col+colspan]) + 3 * (colspan - 1)
145
+
146
+ line << cross(-1, row) if on_border
147
+
148
+ @unknotted[row].each_with_index do |cell, col|
149
+ n = display_row - sum(@cell_size[:row][0, cell.row])
150
+ n -= sum(@border[:row][0, cell.row]) if @options[:border]
151
+ value = cell.attr[:value][n]
152
+
153
+ type =
154
+ unless on_border
155
+ :value
109
156
  else
110
- max[:col][col]
157
+ if row_border_visible?(col, row)
158
+ :cell_border
159
+ elsif !value
160
+ :blank
161
+ else
162
+ :value
163
+ end
111
164
  end
112
165
 
113
- line << justify(cell.attr[:value][n].to_s, width)
166
+ case type
167
+ when :cell_border
168
+ line << cell_border(col)
169
+ when :blank
170
+ line << " " * cell_width(col)
171
+ when :value
172
+ next unless cell.col == col
173
+ line << cell_format % align(value.to_s, width_with_colspan(cell), cell.attr[:align] || @options[:align])
174
+ end
175
+
176
+ line << cross(col, row) if on_border
114
177
  end
115
178
 
116
- result += "| " + line.join(" | ") + " |\n"
179
+ if on_border
180
+ lines << line.join
181
+ else
182
+ lines << line_format % line.join(@options[:col_border])
183
+ end
184
+ end
185
+
186
+ lines << row_border(@num_rows - 1) if @options[:border] && @options[:border][:bottom]
187
+
188
+ lines
189
+ end
190
+
191
+ def width_with_colspan(cell)
192
+ col = cell.col
193
+ if Span === cell && colspan = cell.attr[:colspan]
194
+ borders = sum(@border[:col][col, colspan - 1])
195
+ sum(@cell_size[:col][col, colspan]) + column_border_width * borders
196
+ else
197
+ @cell_size[:col][col]
117
198
  end
199
+ end
118
200
 
119
- result
201
+ def row_border(row)
202
+ line = ""
203
+ line << cross(-1, row)
204
+ @num_cols.times do |col|
205
+ line << cell_border(col)
206
+ line << cross(col, row)
207
+ end
208
+ line
209
+ end
210
+
211
+ def cell_width(col)
212
+ @cell_size[:col][col] + @options[:padding].display_width * 2
213
+ end
214
+
215
+ def cell_border(col)
216
+ @options[:row_border] * cell_width(col)
217
+ end
218
+
219
+ def row_border_visible?(col, row)
220
+ !@unknotted[row + 1] || @unknotted[row][col] != @unknotted[row + 1][col]
221
+ end
222
+
223
+ def cross(col, row)
224
+ if @unknotted[row+1]
225
+ sw = @unknotted[row+1][col]
226
+ se = @unknotted[row+1][col+1]
227
+ else
228
+ sw = se = nil
229
+ end
230
+
231
+ nw = @unknotted[row][col]
232
+ ne = @unknotted[row][col+1]
233
+
234
+ nw = ne = nil if row < 0
235
+ sw = nw = nil if col < 0
236
+
237
+ case
238
+ when nw == ne && sw == se
239
+ @options[:row_border]
240
+ when nw == sw && ne == se
241
+ @options[:col_border]
242
+ else
243
+ @options[:cross]
244
+ end
245
+ end
246
+
247
+ def normalize(cell)
248
+ unless Hash === cell
249
+ cell = {:value => cell}
250
+ end
251
+
252
+ cell.delete :colspan if cell[:colspan] == 1
253
+ cell.delete :rowspan if cell[:rowspan] == 1
254
+
255
+ cell[:value] = cell[:value].to_s.lines.map(&:strip)
256
+
257
+ cell
120
258
  end
121
259
 
122
260
  def sum(array)
123
- array.inject(0) {|r, i| r + i }
261
+ array.to_a.inject(0) {|r, i| r + i }
124
262
  end
125
263
 
126
- def justify(str, width, type=:right)
264
+ def align(str, width, type=:auto)
127
265
  pad = width - str.display_width
128
- pad = 0 if pad < 0
266
+ pad = 0 if pad < 0
129
267
  case type
130
- when :right
131
- " " * pad + str
268
+ when :auto
269
+ if width * 0.85 < pad
270
+ align(str, width, :center)
271
+ else
272
+ align(str, width, :right)
273
+ end
132
274
  when :left
133
275
  str + " " * pad
134
276
  when :center
135
277
  " " * (pad / 2) + str + " " * (pad - pad / 2)
278
+ else # :right
279
+ " " * pad + str
136
280
  end
137
281
  end
138
282
  end
@@ -1,28 +1,26 @@
1
+ #!ruby -Ku -I../lib -rubygems
1
2
  # -*- coding: UTF-8 -*-;
2
3
  require "test/unit"
3
4
  require "text_layout"
4
5
 
5
- $KCODE = 'u'
6
-
7
6
  class TestTextLayout < Test::Unit::TestCase
8
- def test_wrap_private
9
- wrap = TextLayout::Wrap.new("")
10
- assert_equal %w(linew rap), wrap.send(:split_word, "linewrap", 5)
11
- assert_equal %w(あい うえ), wrap.send(:split_word, "あいうえ", 5)
12
- end
13
-
14
- def test_wrap
15
- assert_equal (["+"*20] * 4).join("\n"), TextLayout::Wrap.new("+"*80).layout(20)
16
- assert_equal "line-\nwrap", TextLayout::Wrap.new("linewrap").layout(5)
7
+ def test_span_1
8
+ assert_table [
9
+ [{:value => 1, :colspan => 1, :rowspan => 1}],
10
+ ], <<-RESULT
11
+ | 1 |
12
+ RESULT
17
13
  end
18
14
 
19
15
  def test_table_simple
20
16
  assert_table [
21
17
  [1, 2],
22
- [3, 4]
18
+ [3, 4],
19
+ [5, 6]
23
20
  ], <<-RESULT
24
21
  | 1 | 2 |
25
22
  | 3 | 4 |
23
+ | 5 | 6 |
26
24
  RESULT
27
25
  end
28
26
 
@@ -57,7 +55,49 @@ class TestTextLayout < Test::Unit::TestCase
57
55
  RESULT
58
56
  end
59
57
 
60
- def assert_table(array, result)
61
- assert_equal result, TextLayout::Table.new(array).layout
58
+ def test_align
59
+ assert_table [
60
+ [{:value => 1, :align => :left}, {:value => 2, :align => :center}, {:value => 3, :align => :right}],
61
+ [ {:value => "oooooooooooooooooooo", :colspan => 3}]
62
+ ], <<-RESULT
63
+ | 1 | 2 | 3 |
64
+ | oooooooooooooooooooo |
65
+ RESULT
66
+ end
67
+
68
+ def test_successive_colspan
69
+ assert_table [[{:colspan => 2, :value => "a"}] * 2], <<-RESULT
70
+ | a | a |
71
+ RESULT
72
+ end
73
+
74
+ def test_border
75
+ assert_table [
76
+ [{:value => "", :colspan => 3}],
77
+ [1,2,3],
78
+ [{:value => "a", :rowspan=>2},"a",{:value => "a\na", :rowspan => 2}],
79
+ ["a"]
80
+ ], <<-RESULT, :border => true
81
+ +-----------+
82
+ | |
83
+ +---+---+---+
84
+ | 1 | 2 | 3 |
85
+ +---+---+---+
86
+ | a | a | a |
87
+ | +---+ a |
88
+ | | a | |
89
+ +---+---+---+
90
+ RESULT
91
+ end
92
+
93
+ def assert_table(array, expected, options={})
94
+ result = TextLayout::Table.new(array, options).layout
95
+
96
+ if expected.rstrip != result
97
+ puts
98
+ puts result
99
+ end
100
+
101
+ assert_equal expected.rstrip, result
62
102
  end
63
103
  end
@@ -0,0 +1,17 @@
1
+ #!ruby -Ku -I../lib -rubygems
2
+ # -*- coding: UTF-8 -*-;
3
+ require "test/unit"
4
+ require "text_layout"
5
+
6
+ class TestWrap < Test::Unit::TestCase
7
+ def test_wrap_private
8
+ wrap = TextLayout::Wrap.new("")
9
+ assert_equal %w(linew rap), wrap.send(:split_word, "linewrap", 5)
10
+ assert_equal %w(あい うえ), wrap.send(:split_word, "あいうえ", 5)
11
+ end
12
+
13
+ def test_wrap
14
+ assert_equal (["+"*20] * 4).join("\n"), TextLayout::Wrap.new("+"*80).layout(20)
15
+ assert_equal "line-\nwrap", TextLayout::Wrap.new("linewrap").layout(5)
16
+ end
17
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: text_layout
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -9,11 +9,11 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2011-09-14 00:00:00.000000000Z
12
+ date: 2011-09-16 00:00:00.000000000Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: unicode-display_width
16
- requirement: &70140059189400 !ruby/object:Gem::Requirement
16
+ requirement: &70135698111700 !ruby/object:Gem::Requirement
17
17
  none: false
18
18
  requirements:
19
19
  - - ! '>='
@@ -21,10 +21,10 @@ dependencies:
21
21
  version: '0'
22
22
  type: :runtime
23
23
  prerelease: false
24
- version_requirements: *70140059189400
24
+ version_requirements: *70135698111700
25
25
  - !ruby/object:Gem::Dependency
26
26
  name: hoe
27
- requirement: &70140059188880 !ruby/object:Gem::Requirement
27
+ requirement: &70135698111120 !ruby/object:Gem::Requirement
28
28
  none: false
29
29
  requirements:
30
30
  - - ~>
@@ -32,8 +32,8 @@ dependencies:
32
32
  version: '2.10'
33
33
  type: :development
34
34
  prerelease: false
35
- version_requirements: *70140059188880
36
- description: Text layout engine for CUI apps.
35
+ version_requirements: *70135698111120
36
+ description: Text layout engine for CUI apps with a pretty table.
37
37
  email:
38
38
  - nanki@dotswitch.net
39
39
  executables: []
@@ -50,6 +50,7 @@ files:
50
50
  - Rakefile
51
51
  - README.rdoc
52
52
  - test/test_text_layout.rb
53
+ - test/test_wrap.rb
53
54
  - .gemtest
54
55
  homepage: http://github.com/nanki/text_layout
55
56
  licenses: []
@@ -76,6 +77,7 @@ rubyforge_project: text_layout
76
77
  rubygems_version: 1.8.6
77
78
  signing_key:
78
79
  specification_version: 3
79
- summary: Text layout engine for CUI apps.
80
+ summary: Text layout engine for CUI apps with a pretty table.
80
81
  test_files:
81
82
  - test/test_text_layout.rb
83
+ - test/test_wrap.rb