marktable 0.0.5 → 0.1.1
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.
- checksums.yaml +4 -4
- data/README.md +65 -11
- data/lib/marktable/formatters/base.rb +27 -0
- data/lib/marktable/formatters/csv.rb +24 -0
- data/lib/marktable/formatters/html.rb +50 -0
- data/lib/marktable/formatters/markdown.rb +66 -0
- data/lib/marktable/row.rb +74 -62
- data/lib/marktable/table.rb +53 -123
- data/lib/marktable/tables/array.rb +57 -0
- data/lib/marktable/tables/base.rb +33 -0
- data/lib/marktable/tables/csv.rb +71 -0
- data/lib/marktable/tables/html.rb +67 -0
- data/lib/marktable/tables/markdown.rb +82 -0
- data/lib/marktable/version.rb +5 -0
- data/lib/marktable.rb +10 -126
- data/spec/support/matchers/markdown_matchers.rb +34 -132
- metadata +29 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 776f59a6e0c0ee0b8e52f3fd4b9dd7a8ae8e1acbb93ff98504bb382dfecc93e9
|
4
|
+
data.tar.gz: 8df1d4b723562ced9f83e172165ed07d934e95fe2d9fe7c39411371b960dadfb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed5b2e94ca382771d81d5c6423fa43e38dded1cbfe315c6fedd098b0f77efc66d18b7e62372f8ac2e16884a3d3495f5631fee13b96623d8575276e8b6827b619
|
7
|
+
data.tar.gz: 28d859a6e6c1709ee0be5c3ff7f63fd91f87688d6c5169d0193790889e590ce4b0602afa7c1a5f48693227bf963cca9396a26900f037899351d4c54c69783833
|
data/README.md
CHANGED
@@ -1,6 +1,24 @@
|
|
1
1
|
# Marktable
|
2
2
|
|
3
|
-
Marktable is a Ruby gem for
|
3
|
+
Marktable is a Ruby gem for easily converting between different table formats and testing table content in your specs.
|
4
|
+
|
5
|
+
## Table of Contents
|
6
|
+
|
7
|
+
- [Features](#features)
|
8
|
+
- [Installation](#installation)
|
9
|
+
- [Basic Usage](#basic-usage)
|
10
|
+
- [Documentation](#documentation)
|
11
|
+
- [License](#license)
|
12
|
+
|
13
|
+
## Features
|
14
|
+
|
15
|
+
* RSpec matchers to compare tables across formats
|
16
|
+
* Convert between multiple table formats:
|
17
|
+
- Markdown tables
|
18
|
+
- Arrays of arrays
|
19
|
+
- Arrays of hashes
|
20
|
+
- CSV
|
21
|
+
- HTML tables
|
4
22
|
|
5
23
|
## Installation
|
6
24
|
|
@@ -12,23 +30,59 @@ gem 'marktable'
|
|
12
30
|
|
13
31
|
And then execute:
|
14
32
|
|
15
|
-
```
|
16
|
-
bundle install
|
33
|
+
```
|
34
|
+
$ bundle install
|
35
|
+
```
|
36
|
+
|
37
|
+
Or install it yourself:
|
38
|
+
|
39
|
+
```
|
40
|
+
$ gem install marktable
|
17
41
|
```
|
18
42
|
|
19
|
-
|
43
|
+
## Basic Usage
|
20
44
|
|
21
|
-
|
22
|
-
|
45
|
+
### RSpec Matchers
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
# In your spec_helper.rb
|
49
|
+
require 'marktable/rspec'
|
50
|
+
|
51
|
+
# In your specs - compare tables across formats
|
52
|
+
expect(markdown_table).to match_markdown(expected_markdown)
|
53
|
+
|
54
|
+
html_table = page.find('#users-table')
|
55
|
+
expect(html_table).to match_markdown(expected_markdown)
|
23
56
|
```
|
24
57
|
|
25
|
-
|
58
|
+
### Converting Between Formats
|
26
59
|
|
27
60
|
```ruby
|
28
|
-
|
29
|
-
|
61
|
+
# From markdown to other formats
|
62
|
+
markdown = <<~MARKDOWN
|
63
|
+
| Name | Age |
|
64
|
+
|------|----- |
|
65
|
+
| Alice | 30 |
|
66
|
+
| Bob | 25 |
|
67
|
+
MARKDOWN
|
68
|
+
|
69
|
+
table = Marktable.from_markdown(markdown)
|
70
|
+
|
71
|
+
table.to_a # Array of hashes
|
72
|
+
table.to_csv # CSV string
|
73
|
+
table.to_html # HTML table
|
74
|
+
|
75
|
+
# From arrays or hashes to markdown
|
76
|
+
data = [{ 'Name' => 'Alice', 'Age' => '30' }]
|
77
|
+
Marktable.from_array(data).to_md # Markdown table
|
30
78
|
```
|
31
79
|
|
32
|
-
##
|
80
|
+
## Documentation
|
81
|
+
|
82
|
+
* [Full Examples](docs/examples.md) - Detailed usage examples
|
83
|
+
* [API Documentation](docs/api_documentation.md) - Complete API reference
|
84
|
+
|
85
|
+
## License
|
33
86
|
|
34
|
-
|
87
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
88
|
+
`
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'markdown'
|
4
|
+
# require_relative "array"
|
5
|
+
require_relative 'csv'
|
6
|
+
require_relative 'html'
|
7
|
+
|
8
|
+
module Marktable
|
9
|
+
module Formatters
|
10
|
+
class Base
|
11
|
+
def self.for(type)
|
12
|
+
case type.to_sym
|
13
|
+
when :markdown
|
14
|
+
Markdown
|
15
|
+
when :array
|
16
|
+
Array
|
17
|
+
when :csv
|
18
|
+
CSV
|
19
|
+
when :html
|
20
|
+
HTML
|
21
|
+
else
|
22
|
+
raise ArgumentError, "Unknown table type: #{type}"
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'csv'
|
4
|
+
|
5
|
+
module Marktable
|
6
|
+
module Formatters
|
7
|
+
class CSV
|
8
|
+
def self.format(rows, headers = nil)
|
9
|
+
return '' if rows.empty? && headers.nil?
|
10
|
+
|
11
|
+
::CSV.generate do |csv|
|
12
|
+
csv << headers if headers
|
13
|
+
rows.each do |row|
|
14
|
+
csv << format_row(row)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.format_row(row)
|
20
|
+
row.values.map { |val| val unless val.to_s.empty? }
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module Marktable
|
6
|
+
module Formatters
|
7
|
+
class HTML
|
8
|
+
def self.format(rows, headers = nil)
|
9
|
+
return '' if rows.empty? && headers.nil?
|
10
|
+
|
11
|
+
builder = Nokogiri::HTML::Builder.new do |doc|
|
12
|
+
doc.table do
|
13
|
+
if headers
|
14
|
+
doc.thead do
|
15
|
+
doc.tr do
|
16
|
+
headers.each do |header|
|
17
|
+
doc.th { doc.text header }
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
doc.tbody do
|
24
|
+
rows.each do |row|
|
25
|
+
doc.tr do
|
26
|
+
row.values.each do |cell|
|
27
|
+
doc.td do
|
28
|
+
cell_text = cell.to_s
|
29
|
+
if cell_text.include?('\n')
|
30
|
+
cell_text.split('\n').each_with_index do |line, index|
|
31
|
+
doc.br if index.positive?
|
32
|
+
doc.text line
|
33
|
+
end
|
34
|
+
else
|
35
|
+
doc.text cell_text
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Extract just the table element to avoid including DOCTYPE
|
46
|
+
builder.doc.at_css('table').to_html
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Marktable
|
4
|
+
module Formatters
|
5
|
+
class Markdown
|
6
|
+
def self.format(rows, headers = nil)
|
7
|
+
return '' if rows.empty? && headers.nil?
|
8
|
+
|
9
|
+
# Calculate column widths
|
10
|
+
widths = calculate_column_widths(rows, headers)
|
11
|
+
|
12
|
+
lines = []
|
13
|
+
|
14
|
+
# Add header row if we have headers
|
15
|
+
if headers
|
16
|
+
lines << Row.new(headers).to_markdown(widths)
|
17
|
+
lines << separator_row(widths)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Add data rows
|
21
|
+
rows.each do |row|
|
22
|
+
lines << row.to_markdown(widths)
|
23
|
+
end
|
24
|
+
|
25
|
+
lines.join("\n")
|
26
|
+
end
|
27
|
+
|
28
|
+
def self.calculate_column_widths(rows, headers)
|
29
|
+
# Determine the maximum number of columns to consider
|
30
|
+
max_cols = headers ? headers.size : 0
|
31
|
+
|
32
|
+
if headers.nil?
|
33
|
+
# Without headers, find the maximum number of values across all rows
|
34
|
+
rows.each do |row|
|
35
|
+
max_cols = [max_cols, row.values.size].max
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Initialize widths array with zeros
|
40
|
+
widths = Array.new(max_cols, 0)
|
41
|
+
|
42
|
+
# Process headers if available
|
43
|
+
headers&.each_with_index do |header, i|
|
44
|
+
width = header.to_s.length
|
45
|
+
widths[i] = width if width > widths[i]
|
46
|
+
end
|
47
|
+
|
48
|
+
# Process row values, but only up to the max_cols
|
49
|
+
rows.each do |row|
|
50
|
+
values = row.values.take(max_cols)
|
51
|
+
values.each_with_index do |value, i|
|
52
|
+
width = value.to_s.length
|
53
|
+
widths[i] = width if width > widths[i]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
widths
|
58
|
+
end
|
59
|
+
|
60
|
+
def self.separator_row(widths)
|
61
|
+
separator_parts = widths.map { |width| '-' * width }
|
62
|
+
"| #{separator_parts.join(' | ')} |"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
data/lib/marktable/row.rb
CHANGED
@@ -2,97 +2,109 @@
|
|
2
2
|
|
3
3
|
module Marktable
|
4
4
|
class Row
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :values, :headers
|
6
6
|
|
7
|
-
def initialize(data
|
7
|
+
def initialize(data, headers: nil)
|
8
8
|
@headers = headers
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
9
|
+
@values = extract_values(data)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Format the row as a markdown table row
|
13
|
+
def to_markdown(column_widths)
|
14
|
+
vals = @values
|
15
|
+
|
16
|
+
# Limit values to either headers count or column_widths size, whichever is appropriate
|
17
|
+
max_cols = @headers ? @headers.size : column_widths.size
|
18
|
+
|
19
|
+
formatted_values = column_widths.take(max_cols).map.with_index do |width, i|
|
20
|
+
if i < vals.size
|
21
|
+
vals[i].to_s.ljust(width)
|
22
22
|
else
|
23
|
-
|
24
|
-
data_strings
|
23
|
+
''.ljust(width)
|
25
24
|
end
|
26
|
-
else
|
27
|
-
@data = headers ? {} : []
|
28
25
|
end
|
26
|
+
|
27
|
+
"| #{formatted_values.join(' | ')} |"
|
29
28
|
end
|
30
29
|
|
30
|
+
# Access a value by index or header
|
31
31
|
def [](key)
|
32
|
-
if
|
33
|
-
@
|
34
|
-
elsif
|
35
|
-
@
|
36
|
-
|
37
|
-
nil
|
32
|
+
if key.is_a?(Integer)
|
33
|
+
@values[key]
|
34
|
+
elsif @headers
|
35
|
+
idx = @headers.index(key)
|
36
|
+
idx ? @values[idx] : nil
|
38
37
|
end
|
39
38
|
end
|
40
39
|
|
40
|
+
# Set a value by index or header
|
41
41
|
def []=(key, value)
|
42
|
-
if
|
43
|
-
@
|
44
|
-
elsif
|
45
|
-
|
42
|
+
if key.is_a?(Integer)
|
43
|
+
@values[key] = value if key >= 0 && key < @values.size
|
44
|
+
elsif @headers
|
45
|
+
idx = @headers.index(key)
|
46
|
+
@values[idx] = value if idx && idx < @values.size
|
46
47
|
end
|
47
48
|
end
|
48
49
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
def keys
|
54
|
-
@data.is_a?(Hash) ? @data.keys : (0...@data.size).to_a
|
55
|
-
end
|
56
|
-
|
57
|
-
def to_h
|
58
|
-
return @data if @data.is_a?(Hash)
|
59
|
-
return {} if @data.empty? || @headers.nil? || @headers.empty?
|
60
|
-
|
61
|
-
@headers.each_with_index.each_with_object({}) do |(header, i), hash|
|
62
|
-
hash[header] = i < @data.length ? @data[i] : ''
|
63
|
-
end
|
50
|
+
# Check if this row uses headers
|
51
|
+
def headers?
|
52
|
+
!@headers.nil?
|
64
53
|
end
|
65
54
|
|
66
|
-
|
67
|
-
|
68
|
-
|
55
|
+
# Convert row data to a hash using headers as keys
|
56
|
+
def to_hash
|
57
|
+
return {} unless @headers
|
69
58
|
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
59
|
+
result = {}
|
60
|
+
@values.each_with_index do |value, i|
|
61
|
+
# Only include values that have a corresponding header
|
62
|
+
if i < @headers.size
|
63
|
+
header = @headers[i]
|
64
|
+
result[header] = value if header
|
65
|
+
end
|
75
66
|
end
|
76
|
-
|
67
|
+
result
|
77
68
|
end
|
78
69
|
|
79
70
|
# Parse a markdown row string into an array of values
|
80
71
|
def self.parse(row_string)
|
81
|
-
|
72
|
+
# Skip if nil or empty
|
73
|
+
return [] if row_string.nil? || row_string.strip.empty?
|
74
|
+
|
75
|
+
# Remove leading/trailing pipes and split by pipe
|
76
|
+
cells = row_string.strip.sub(/^\|/, '').sub(/\|$/, '').split('|')
|
77
|
+
|
78
|
+
# Trim whitespace from each cell
|
79
|
+
cells.map(&:strip)
|
82
80
|
end
|
83
81
|
|
84
|
-
# Check if a row
|
82
|
+
# Check if a row is a separator row
|
85
83
|
def self.separator?(row_string)
|
86
|
-
|
84
|
+
# Skip if nil or empty
|
85
|
+
return false if row_string.nil? || row_string.strip.empty?
|
86
|
+
|
87
|
+
# Remove pipes and strip whitespace
|
88
|
+
content = row_string.gsub('|', '').strip
|
89
|
+
|
90
|
+
# Check if it contains only dashes and colons (separator characters)
|
91
|
+
content.match?(/^[\s:,-]+$/) && content.include?('-')
|
87
92
|
end
|
88
93
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
94
|
+
private
|
95
|
+
|
96
|
+
def extract_values(data)
|
97
|
+
case data
|
98
|
+
when Hash
|
99
|
+
if @headers
|
100
|
+
@headers.map { |h| data[h] || '' }
|
101
|
+
else
|
102
|
+
data.values
|
103
|
+
end
|
104
|
+
else
|
105
|
+
# Array or other enumerable
|
106
|
+
Array(data)
|
93
107
|
end
|
94
|
-
|
95
|
-
["| #{separators.join(' | ')} |", column_widths]
|
96
108
|
end
|
97
109
|
end
|
98
110
|
end
|
data/lib/marktable/table.rb
CHANGED
@@ -1,154 +1,84 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'formatters/base'
|
4
|
+
|
3
5
|
module Marktable
|
4
6
|
class Table
|
5
7
|
include Enumerable
|
6
|
-
|
8
|
+
|
7
9
|
attr_reader :headers
|
8
10
|
|
9
|
-
def initialize(
|
10
|
-
|
11
|
-
|
12
|
-
@
|
13
|
-
|
11
|
+
def initialize(source, type: :markdown, headers: nil)
|
12
|
+
parser = Tables::Base.for(type).new(source, headers)
|
13
|
+
result = parser.parse
|
14
|
+
@rows = result.rows
|
15
|
+
@headers = result.headers
|
16
|
+
# Validate headers if present
|
17
|
+
validate_headers if @headers
|
18
|
+
# Fix: Initialize @has_headers based on whether @headers is present
|
19
|
+
@has_headers = !@headers.nil?
|
14
20
|
end
|
15
21
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
else
|
20
|
-
@rows.each
|
21
|
-
end
|
22
|
-
end
|
22
|
+
# Iteration support
|
23
|
+
def each(&block)
|
24
|
+
return enum_for(:each) unless block_given?
|
23
25
|
|
24
|
-
|
25
|
-
@rows.map { |row| row.data }
|
26
|
-
end
|
27
|
-
|
28
|
-
def to_s
|
29
|
-
generate
|
26
|
+
@rows.each(&block)
|
30
27
|
end
|
31
28
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
# Calculate column widths considering both headers and all row values
|
39
|
-
all_values = [keys] + @rows.map { |row| row.values }
|
40
|
-
column_widths = calculate_column_widths(all_values)
|
41
|
-
|
42
|
-
# Build the markdown table
|
43
|
-
build_markdown_table(keys, column_widths)
|
44
|
-
end
|
45
|
-
|
46
|
-
private
|
47
|
-
|
48
|
-
def header_keys
|
49
|
-
if @headers && @header_row
|
50
|
-
@header_row.keys
|
51
|
-
elsif @rows.first&.data.is_a?(Hash)
|
52
|
-
@rows.first.data.keys
|
29
|
+
# Returns the table as an Array of Hashes if headers are present
|
30
|
+
# or Array of Arrays if no headers
|
31
|
+
def to_a
|
32
|
+
if @has_headers
|
33
|
+
# Convert rows to hashes, which will automatically exclude values without headers
|
34
|
+
@rows.map(&:to_hash)
|
53
35
|
else
|
54
|
-
|
36
|
+
# When no headers, return array of arrays with consistent length
|
37
|
+
max_length = @rows.map { |row| row.values.length }.max || 0
|
38
|
+
@rows.map do |row|
|
39
|
+
values = row.values
|
40
|
+
values + Array.new(max_length - values.length, '')
|
41
|
+
end
|
55
42
|
end
|
56
43
|
end
|
57
44
|
|
58
|
-
def
|
59
|
-
|
60
|
-
|
61
|
-
# Add header row
|
62
|
-
result << row_to_markdown(keys, column_widths)
|
63
|
-
|
64
|
-
# Add separator row
|
65
|
-
separator, _ = Row.separator_row(column_widths)
|
66
|
-
result << separator
|
67
|
-
|
68
|
-
# Add data rows
|
69
|
-
rows_to_render.each do |row|
|
70
|
-
result << row.to_markdown(column_widths)
|
71
|
-
end
|
72
|
-
|
73
|
-
result.join("\n")
|
45
|
+
def to_html
|
46
|
+
Formatters::Base.for(:html).format(@rows, @headers)
|
74
47
|
end
|
75
48
|
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
else
|
80
|
-
@rows
|
81
|
-
end
|
49
|
+
# Generate markdown representation
|
50
|
+
def to_md
|
51
|
+
Formatters::Base.for(:markdown).format(@rows, @headers)
|
82
52
|
end
|
53
|
+
alias generate to_md
|
83
54
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
return if rows.empty?
|
88
|
-
|
89
|
-
if @headers
|
90
|
-
parse_with_headers(rows)
|
91
|
-
else
|
92
|
-
parse_without_headers(rows)
|
93
|
-
end
|
55
|
+
# Generate CSV representation
|
56
|
+
def to_csv
|
57
|
+
Formatters::Base.for(:csv).format(@rows, @headers)
|
94
58
|
end
|
95
59
|
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
@header_row = header_values.each_with_object({}) { |val, hash| hash[val] = val }
|
100
|
-
|
101
|
-
# Process each data row
|
102
|
-
rows.each_with_index do |row, index|
|
103
|
-
# Skip header row and separator rows
|
104
|
-
next if index == 0 || Row.separator?(row)
|
105
|
-
|
106
|
-
# Parse the row into values
|
107
|
-
values = Row.parse(row)
|
108
|
-
|
109
|
-
# Create a hash mapping headers to values
|
110
|
-
row_hash = {}
|
111
|
-
header_values.each_with_index do |header, i|
|
112
|
-
row_hash[header] = i < values.length ? values[i] : ''
|
113
|
-
end
|
114
|
-
|
115
|
-
@rows << Row.new(row_hash, headers: header_values)
|
116
|
-
end
|
60
|
+
# Support for accessing by index like table[0]
|
61
|
+
def [](index)
|
62
|
+
@rows[index]
|
117
63
|
end
|
118
64
|
|
119
|
-
|
120
|
-
|
121
|
-
rows.
|
122
|
-
# Skip separator rows
|
123
|
-
next if Row.separator?(row)
|
124
|
-
|
125
|
-
# Parse the row into values
|
126
|
-
values = Row.parse(row)
|
127
|
-
@rows << Row.new(values, headers: nil)
|
128
|
-
end
|
65
|
+
# Returns the number of rows
|
66
|
+
def size
|
67
|
+
@rows.size
|
129
68
|
end
|
69
|
+
alias length size
|
130
70
|
|
131
|
-
|
132
|
-
|
133
|
-
max_column_count = arrays_of_values.map { |row| row.size }.max || 0
|
134
|
-
column_widths = Array.new(max_column_count, 0)
|
135
|
-
|
136
|
-
arrays_of_values.each do |row|
|
137
|
-
row.each_with_index do |cell, i|
|
138
|
-
cell_width = cell.to_s.length
|
139
|
-
column_widths[i] = [column_widths[i], cell_width].max
|
140
|
-
end
|
141
|
-
end
|
142
|
-
|
143
|
-
column_widths
|
71
|
+
def empty?
|
72
|
+
@rows.empty?
|
144
73
|
end
|
145
74
|
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
75
|
+
private
|
76
|
+
|
77
|
+
def validate_headers
|
78
|
+
duplicates = @headers.group_by { |h| h }.select { |_, v| v.size > 1 }.keys
|
79
|
+
return unless duplicates.any?
|
80
|
+
|
81
|
+
raise ArgumentError, "Duplicate headers are not allowed: #{duplicates.join(', ')}"
|
152
82
|
end
|
153
83
|
end
|
154
84
|
end
|