text_layout 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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