excel_utils 1.0.0 → 1.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/excel_utils.gemspec +3 -2
- data/lib/excel_utils.rb +15 -5
- data/lib/excel_utils/sheets/base.rb +41 -0
- data/lib/excel_utils/sheets/csv.rb +28 -0
- data/lib/excel_utils/sheets/excel.rb +35 -0
- data/lib/excel_utils/sheets/excel_stream.rb +35 -0
- data/lib/excel_utils/version.rb +2 -2
- data/lib/excel_utils/workbooks/csv.rb +36 -0
- data/lib/excel_utils/workbooks/excel.rb +41 -0
- data/lib/excel_utils/writer.rb +17 -8
- data/spec/minitest_helper.rb +24 -1
- data/spec/read_spec.rb +113 -66
- data/spec/resources/basic.csv +5 -0
- data/spec/resources/basic.ods +0 -0
- data/spec/{sample.xls → resources/basic.xls} +0 -0
- data/spec/resources/basic.xlsm +0 -0
- data/spec/resources/basic.xlsx +0 -0
- data/spec/resources/custom_extension.tmp +0 -0
- data/spec/resources/empty.csv +0 -0
- data/spec/resources/multiple.xlsx +0 -0
- data/spec/resources/only_headers.csv +1 -0
- data/spec/write_spec.rb +80 -17
- metadata +44 -12
- data/lib/excel_utils/sheet.rb +0 -50
- data/lib/excel_utils/workbook.rb +0 -33
- data/spec/sample.xlsx +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 70807b62912a2e82da93e85e0275b6f3f6b6a8f010f70a7924546f31f0d93087
|
4
|
+
data.tar.gz: 6b78f90fd4174174044c9464299bf645bc13619251b1fde6f0ac9079a00b6d14
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c8619bbea23bbad8b25c4014e0ead0bb8b81d901dce0c0e355e47b3b1fd2742f7c307d25980afc7d5066515ef33547d89d8c71df586ffe06bcdd3bc5dc6f9de4
|
7
|
+
data.tar.gz: 98a84f2fa77259e4ce3bbb691d267d4427e7e887eb0a41385ee7b82e59b7e1e6d06e1da333a257f3cdc9c54768a530c7c709608c3e8b89726c89ae61185b46fa
|
data/excel_utils.gemspec
CHANGED
@@ -19,9 +19,10 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
|
21
21
|
spec.add_runtime_dependency 'inflecto', '~> 0.0'
|
22
|
-
spec.add_runtime_dependency 'roo', '~> 2.
|
23
|
-
spec.add_runtime_dependency 'roo-xls', '~> 1.
|
22
|
+
spec.add_runtime_dependency 'roo', '~> 2.8'
|
23
|
+
spec.add_runtime_dependency 'roo-xls', '~> 1.2'
|
24
24
|
spec.add_runtime_dependency 'write_xlsx', '~> 0.85'
|
25
|
+
spec.add_runtime_dependency 'nesquikcsv', '~> 0.1'
|
25
26
|
|
26
27
|
spec.add_development_dependency 'rake', '~> 12.0'
|
27
28
|
spec.add_development_dependency 'minitest', '~> 5.0', '< 5.11'
|
data/lib/excel_utils.rb
CHANGED
@@ -4,20 +4,30 @@ require 'roo'
|
|
4
4
|
require 'roo-xls'
|
5
5
|
require 'write_xlsx'
|
6
6
|
require 'inflecto'
|
7
|
+
require 'nesquikcsv'
|
7
8
|
|
8
9
|
require_relative 'excel_utils/version'
|
9
|
-
require_relative 'excel_utils/
|
10
|
-
require_relative 'excel_utils/
|
10
|
+
require_relative 'excel_utils/workbooks/csv'
|
11
|
+
require_relative 'excel_utils/workbooks/excel'
|
12
|
+
require_relative 'excel_utils/sheets/base'
|
13
|
+
require_relative 'excel_utils/sheets/csv'
|
14
|
+
require_relative 'excel_utils/sheets/excel'
|
15
|
+
require_relative 'excel_utils/sheets/excel_stream'
|
11
16
|
require_relative 'excel_utils/writer'
|
12
17
|
|
13
18
|
module ExcelUtils
|
14
|
-
|
19
|
+
|
15
20
|
def self.read(filename, **options)
|
16
|
-
|
21
|
+
extension = options.fetch(:extension, File.extname(filename)[1..-1])
|
22
|
+
if extension == 'csv'
|
23
|
+
Workbooks::CSV.new(filename, **options)
|
24
|
+
else
|
25
|
+
Workbooks::Excel.new(filename, **options)
|
26
|
+
end
|
17
27
|
end
|
18
28
|
|
19
29
|
def self.write(filename, data)
|
20
30
|
Writer.write filename, data
|
21
31
|
end
|
22
32
|
|
23
|
-
end
|
33
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ExcelUtils
|
2
|
+
module Sheets
|
3
|
+
class Base
|
4
|
+
|
5
|
+
include Enumerable
|
6
|
+
|
7
|
+
attr_reader :name, :normalize_column_names
|
8
|
+
|
9
|
+
def initialize(name:, normalize_column_names: false)
|
10
|
+
@name = name
|
11
|
+
@normalize_column_names = normalize_column_names
|
12
|
+
end
|
13
|
+
|
14
|
+
def column_names
|
15
|
+
@column_names ||= normalize_column_names ? normalize_columns(first_row) : first_row
|
16
|
+
end
|
17
|
+
|
18
|
+
def each
|
19
|
+
if column_names.any?
|
20
|
+
each_row do |row|
|
21
|
+
break if empty_row? row
|
22
|
+
yield Hash[column_names.zip(row)]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def normalize_columns(names)
|
30
|
+
names.map do |name|
|
31
|
+
Inflecto.underscore(name.strip.gsub(' ', '_')).to_sym
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def empty_row?(row)
|
36
|
+
row.all? { |cell| cell.to_s.strip.empty? }
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module ExcelUtils
|
2
|
+
module Sheets
|
3
|
+
class CSV < Base
|
4
|
+
|
5
|
+
def initialize(filename:, **options)
|
6
|
+
super(**options)
|
7
|
+
@filename = filename
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
attr_reader :filename
|
13
|
+
|
14
|
+
def first_row
|
15
|
+
NesquikCSV.open(filename) { |csv| csv.readline } || []
|
16
|
+
end
|
17
|
+
|
18
|
+
def each_row
|
19
|
+
first = true
|
20
|
+
NesquikCSV.foreach(filename) do |row|
|
21
|
+
yield row unless first
|
22
|
+
first = false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ExcelUtils
|
2
|
+
module Sheets
|
3
|
+
class Excel < Base
|
4
|
+
|
5
|
+
def initialize(spreadsheet:, **options)
|
6
|
+
super(**options)
|
7
|
+
@spreadsheet = spreadsheet
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
attr_reader :spreadsheet
|
13
|
+
|
14
|
+
def first_row
|
15
|
+
with_sheet do |sheet|
|
16
|
+
sheet.first_row ? sheet.row(sheet.first_row) : []
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def each_row
|
21
|
+
with_sheet do |sheet|
|
22
|
+
(sheet.first_row + 1).upto(sheet.last_row) do |i|
|
23
|
+
yield sheet.row(i)
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def with_sheet
|
29
|
+
yield spreadsheet.sheet(name)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module ExcelUtils
|
2
|
+
module Sheets
|
3
|
+
class ExcelStream < Base
|
4
|
+
|
5
|
+
def initialize(spreadsheet:, **options)
|
6
|
+
super(**options)
|
7
|
+
@spreadsheet = spreadsheet
|
8
|
+
end
|
9
|
+
|
10
|
+
private
|
11
|
+
|
12
|
+
attr_reader :spreadsheet
|
13
|
+
|
14
|
+
def first_row
|
15
|
+
row = sheet.each_row_streaming(pad_cells: true, max_rows: 0).first || []
|
16
|
+
normalize_row row
|
17
|
+
end
|
18
|
+
|
19
|
+
def each_row
|
20
|
+
sheet.each_row_streaming(pad_cells: true, offset: 1) do |row|
|
21
|
+
yield normalize_row(row)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def normalize_row(row)
|
26
|
+
row.map { |cell| cell ? cell.value : cell }
|
27
|
+
end
|
28
|
+
|
29
|
+
def sheet
|
30
|
+
spreadsheet.sheet name
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
data/lib/excel_utils/version.rb
CHANGED
@@ -1,3 +1,3 @@
|
|
1
1
|
module ExcelUtils
|
2
|
-
VERSION = '1.
|
3
|
-
end
|
2
|
+
VERSION = '1.3.0'
|
3
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module ExcelUtils
|
2
|
+
module Workbooks
|
3
|
+
class CSV
|
4
|
+
|
5
|
+
SHEET_NAME = 'default'.freeze
|
6
|
+
|
7
|
+
attr_reader :filename, :normalize_column_names
|
8
|
+
|
9
|
+
def initialize(filename, normalize_column_names: false)
|
10
|
+
@filename = filename
|
11
|
+
@normalize_column_names = normalize_column_names
|
12
|
+
|
13
|
+
@sheet = Sheets::CSV.new name: SHEET_NAME,
|
14
|
+
normalize_column_names: normalize_column_names,
|
15
|
+
filename: filename
|
16
|
+
end
|
17
|
+
|
18
|
+
def sheets
|
19
|
+
[sheet]
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](sheet_name)
|
23
|
+
sheet_name == SHEET_NAME ? sheet : nil
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_h
|
27
|
+
{SHEET_NAME => sheet.to_a}
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :sheet
|
33
|
+
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module ExcelUtils
|
2
|
+
module Workbooks
|
3
|
+
class Excel
|
4
|
+
|
5
|
+
attr_reader :filename, :normalize_column_names
|
6
|
+
|
7
|
+
def initialize(filename, normalize_column_names: false, extension: nil)
|
8
|
+
@filename = filename
|
9
|
+
@normalize_column_names = normalize_column_names
|
10
|
+
@spreadsheet = Roo::Spreadsheet.open filename, extension: extension
|
11
|
+
end
|
12
|
+
|
13
|
+
def sheets
|
14
|
+
@sheets ||= spreadsheet.sheets.map do |name|
|
15
|
+
sheet_class.new name: name,
|
16
|
+
normalize_column_names: normalize_column_names,
|
17
|
+
spreadsheet: spreadsheet
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def [](sheet_name)
|
22
|
+
sheets.detect { |sheet| sheet.name == sheet_name }
|
23
|
+
end
|
24
|
+
|
25
|
+
def to_h
|
26
|
+
sheets.each_with_object({}) do |sheet, hash|
|
27
|
+
hash[sheet.name] = sheet.to_a
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
attr_reader :spreadsheet
|
34
|
+
|
35
|
+
def sheet_class
|
36
|
+
@sheet_class ||= spreadsheet.respond_to?(:each_row_streaming) ? Sheets::ExcelStream : Sheets::Excel
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/excel_utils/writer.rb
CHANGED
@@ -1,20 +1,24 @@
|
|
1
1
|
module ExcelUtils
|
2
2
|
class Writer
|
3
3
|
|
4
|
-
|
4
|
+
DEFAULT_SHEET_NAME = 'Sheet1'.freeze
|
5
|
+
|
6
|
+
TIME_FORMAT = '%Y-%m-%dT%H:%M:%S'.freeze
|
5
7
|
|
6
8
|
EXCEL_FORMATS = {
|
7
9
|
date: 'yyyy-mm-dd',
|
8
10
|
date_time: 'yyyy-mm-dd hh:mm:ss'
|
9
|
-
}
|
11
|
+
}.freeze
|
10
12
|
|
11
13
|
class << self
|
12
|
-
|
14
|
+
|
13
15
|
def write(filename, data)
|
14
16
|
workbook = WriteXLSX.new filename, strings_to_urls: false
|
15
17
|
|
16
18
|
formats = add_formats workbook
|
17
19
|
|
20
|
+
data = {DEFAULT_SHEET_NAME => data} if data.is_a? Array
|
21
|
+
|
18
22
|
data.each do |sheet_name, sheet_data|
|
19
23
|
add_sheet workbook, sheet_name, sheet_data, formats
|
20
24
|
end
|
@@ -34,18 +38,23 @@ module ExcelUtils
|
|
34
38
|
sheet = workbook.add_worksheet sheet_name
|
35
39
|
|
36
40
|
if sheet_data.any?
|
37
|
-
header = sheet_data.
|
41
|
+
header = sheet_data.flat_map(&:keys).uniq
|
38
42
|
sheet.write_row 0, 0, header.map(&:to_s)
|
39
43
|
|
40
44
|
sheet_data.each_with_index do |row, r|
|
41
|
-
|
45
|
+
row_index = r + 1
|
46
|
+
header.each_with_index do |column, col_index|
|
42
47
|
if row[column]
|
43
|
-
if row[column].
|
48
|
+
if row[column].is_a?(String) || row[column].is_a?(Array)
|
49
|
+
sheet.write_string row_index, col_index, row[column]
|
50
|
+
|
51
|
+
elsif row[column].respond_to? :to_time
|
44
52
|
time = row[column].to_time
|
45
53
|
type = date?(time) ? :date : :date_time
|
46
|
-
sheet.write_date_time
|
54
|
+
sheet.write_date_time row_index, col_index, time.to_time.strftime(TIME_FORMAT), formats[type]
|
55
|
+
|
47
56
|
else
|
48
|
-
sheet.write
|
57
|
+
sheet.write row_index, col_index, row[column]
|
49
58
|
end
|
50
59
|
end
|
51
60
|
end
|
data/spec/minitest_helper.rb
CHANGED
@@ -3,4 +3,27 @@ require 'minitest/autorun'
|
|
3
3
|
require 'minitest/colorin'
|
4
4
|
require 'pry-nav'
|
5
5
|
|
6
|
-
require 'excel_utils'
|
6
|
+
require 'excel_utils'
|
7
|
+
|
8
|
+
RESOURCES_PATH = File.expand_path '../resources', __FILE__
|
9
|
+
|
10
|
+
TMP_PATH = File.expand_path '../../tmp', __FILE__
|
11
|
+
FileUtils.mkpath TMP_PATH unless Dir.exists? TMP_PATH
|
12
|
+
|
13
|
+
class Minitest::Test
|
14
|
+
|
15
|
+
def resource_path(relative_path)
|
16
|
+
File.join RESOURCES_PATH, relative_path
|
17
|
+
end
|
18
|
+
|
19
|
+
def tmp_path(relative_path)
|
20
|
+
File.join TMP_PATH, relative_path
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
24
|
+
|
25
|
+
Minitest.after_run do
|
26
|
+
Dir.glob(File.join(TMP_PATH, '*')).each do |filename|
|
27
|
+
FileUtils.remove_file filename
|
28
|
+
end
|
29
|
+
end
|
data/spec/read_spec.rb
CHANGED
@@ -2,104 +2,151 @@ require 'minitest_helper'
|
|
2
2
|
|
3
3
|
describe ExcelUtils, 'Read' do
|
4
4
|
|
5
|
-
|
6
|
-
rows_by_sheet[sheet.name].map { |r| Hash[columns_by_sheet[sheet.name].zip(r)] }
|
7
|
-
end
|
5
|
+
Dir.glob(File.join(RESOURCES_PATH, 'basic.*')).each do |filename|
|
8
6
|
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
],
|
17
|
-
'Sheet2' => [
|
18
|
-
[123.0, 'Text 1'],
|
19
|
-
[456.0, 'Text 2']
|
20
|
-
],
|
21
|
-
'Sheet3' => []
|
22
|
-
}
|
23
|
-
end
|
7
|
+
[true, false].each do |normalize_column_names|
|
8
|
+
|
9
|
+
describe File.basename(filename), "Workbook (normalize_column_names: #{normalize_column_names})" do
|
10
|
+
|
11
|
+
let(:workbook) { ExcelUtils.read filename, normalize_column_names: normalize_column_names }
|
12
|
+
|
13
|
+
let(:sheet) { workbook.sheets.first }
|
24
14
|
|
25
|
-
|
15
|
+
let(:csv?) { File.extname(filename) == '.csv' }
|
26
16
|
|
27
|
-
|
28
|
-
|
29
|
-
let(:filename) { File.expand_path "../sample.#{extension}", __FILE__ }
|
17
|
+
let(:expected_sheet_name) { csv? ? 'default' : 'Sheet1' }
|
30
18
|
|
31
|
-
|
19
|
+
let(:column_a) { normalize_column_names ? :column_a : 'Column A' }
|
20
|
+
|
21
|
+
let(:column_b) { normalize_column_names ? :column_b : 'Column B' }
|
22
|
+
|
23
|
+
let(:expected_columns) { [column_a, column_b] }
|
24
|
+
|
25
|
+
let :expected_rows do
|
26
|
+
if csv?
|
27
|
+
[
|
28
|
+
{column_a => '1', column_b => 'some text'},
|
29
|
+
{column_a => '2', column_b => '1,35'},
|
30
|
+
{column_a => '3', column_b => '17/08/2019'},
|
31
|
+
{column_a => '4', column_b => nil}
|
32
|
+
]
|
33
|
+
else
|
34
|
+
[
|
35
|
+
{column_a => 1, column_b => 'some text'},
|
36
|
+
{column_a => 2, column_b => 1.35},
|
37
|
+
{column_a => 3, column_b => Date.parse('2019-08-17')},
|
38
|
+
{column_a => 4, column_b => nil}
|
39
|
+
]
|
40
|
+
end
|
41
|
+
end
|
32
42
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
43
|
+
it 'filename' do
|
44
|
+
workbook.filename.must_equal filename
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'normalize_column_names' do
|
48
|
+
workbook.normalize_column_names.must_equal normalize_column_names
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'sheets' do
|
52
|
+
workbook.sheets.count.must_equal 1
|
53
|
+
workbook[workbook.sheets.first.name].must_equal sheet
|
41
54
|
end
|
42
55
|
|
43
56
|
it 'to_h' do
|
44
|
-
workbook.to_h.must_equal
|
45
|
-
'Sheet2' => expected_rows(workbook['Sheet2']),
|
46
|
-
'Sheet3' => []
|
57
|
+
workbook.to_h.must_equal workbook.sheets.first.name => sheet.to_a
|
47
58
|
end
|
48
59
|
|
49
|
-
|
60
|
+
describe 'Sheet' do
|
50
61
|
|
51
|
-
|
62
|
+
it 'name' do
|
63
|
+
sheet.name.must_equal expected_sheet_name
|
64
|
+
end
|
52
65
|
|
53
|
-
|
66
|
+
it 'normalize_column_names' do
|
67
|
+
sheet.normalize_column_names.must_equal workbook.normalize_column_names
|
68
|
+
end
|
54
69
|
|
55
|
-
|
56
|
-
|
57
|
-
|
70
|
+
it 'column_names' do
|
71
|
+
sheet.column_names.must_equal expected_columns
|
72
|
+
end
|
58
73
|
|
59
|
-
|
60
|
-
|
61
|
-
|
74
|
+
it 'count' do
|
75
|
+
sheet.count.must_equal 4
|
76
|
+
end
|
62
77
|
|
78
|
+
it 'to_a' do
|
79
|
+
sheet.to_a.must_equal expected_rows
|
63
80
|
end
|
64
81
|
|
65
82
|
end
|
66
83
|
|
67
84
|
end
|
68
85
|
|
69
|
-
|
86
|
+
end
|
87
|
+
|
88
|
+
end
|
70
89
|
|
71
|
-
|
72
|
-
|
73
|
-
let :columns_by_sheet do
|
74
|
-
{
|
75
|
-
'Sheet1' => [:column_a, :column_b],
|
76
|
-
'Sheet2' => [:id, :value],
|
77
|
-
'Sheet3' => []
|
78
|
-
}
|
79
|
-
end
|
90
|
+
it 'empty.csv' do
|
91
|
+
workbook = ExcelUtils.read resource_path('empty.csv')
|
80
92
|
|
81
|
-
|
93
|
+
workbook.sheets.map(&:name).must_equal ['default']
|
82
94
|
|
83
|
-
|
95
|
+
workbook['default'].column_names.must_equal []
|
96
|
+
workbook['default'].to_a.must_equal []
|
84
97
|
|
85
|
-
|
98
|
+
workbook.to_h.must_equal 'default' => []
|
99
|
+
end
|
86
100
|
|
87
|
-
|
88
|
-
|
89
|
-
end
|
101
|
+
it 'only_headers.csv' do
|
102
|
+
workbook = ExcelUtils.read resource_path('only_headers.csv')
|
90
103
|
|
91
|
-
|
92
|
-
sheet.to_a.must_equal expected_rows(sheet)
|
93
|
-
end
|
104
|
+
workbook.sheets.map(&:name).must_equal ['default']
|
94
105
|
|
95
|
-
|
106
|
+
workbook['default'].column_names.must_equal ['ID', 'Value']
|
107
|
+
workbook['default'].to_a.must_equal []
|
96
108
|
|
97
|
-
|
109
|
+
workbook.to_h.must_equal 'default' => []
|
110
|
+
end
|
98
111
|
|
99
|
-
|
112
|
+
it 'custom_extension.tmp' do
|
113
|
+
workbook = ExcelUtils.read resource_path('custom_extension.tmp'), extension: 'xlsx'
|
100
114
|
|
101
|
-
|
115
|
+
expected_rows = [
|
116
|
+
{'ID' => 1, 'Value' => 'Text 1'},
|
117
|
+
{'ID' => 2, 'Value' => 'Text 2'}
|
118
|
+
]
|
119
|
+
|
120
|
+
workbook.sheets.map(&:name).must_equal ['Sheet1']
|
121
|
+
|
122
|
+
workbook['Sheet1'].column_names.must_equal ['ID', 'Value']
|
123
|
+
workbook['Sheet1'].to_a.must_equal expected_rows
|
124
|
+
|
125
|
+
workbook.to_h.must_equal 'Sheet1' => expected_rows
|
126
|
+
end
|
127
|
+
|
128
|
+
it 'multiple.xlsx' do
|
129
|
+
workbook = ExcelUtils.read resource_path('multiple.xlsx'), normalize_column_names: true
|
130
|
+
|
131
|
+
workbook.sheets.map(&:name).must_equal ['Sheet1', 'Sheet2', 'Sheet3']
|
132
|
+
|
133
|
+
expected_rows_sheet_1 = [
|
134
|
+
{id: 1, value: 'Text 1'},
|
135
|
+
{id: 2, value: 'Text 2'}
|
136
|
+
]
|
137
|
+
|
138
|
+
workbook['Sheet1'].column_names.must_equal [:id, :value]
|
139
|
+
workbook['Sheet1'].to_a.must_equal expected_rows_sheet_1
|
140
|
+
|
141
|
+
workbook['Sheet2'].column_names.must_equal [:id, :value]
|
142
|
+
workbook['Sheet2'].to_a.must_equal []
|
143
|
+
|
144
|
+
workbook['Sheet3'].column_names.must_equal []
|
145
|
+
workbook['Sheet3'].to_a.must_equal []
|
102
146
|
|
147
|
+
workbook.to_h.must_equal 'Sheet1' => expected_rows_sheet_1,
|
148
|
+
'Sheet2' => [],
|
149
|
+
'Sheet3' => []
|
103
150
|
end
|
104
151
|
|
105
152
|
end
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
Binary file
|
File without changes
|
Binary file
|
@@ -0,0 +1 @@
|
|
1
|
+
ID,Value
|
data/spec/write_spec.rb
CHANGED
@@ -2,32 +2,95 @@ require 'minitest_helper'
|
|
2
2
|
|
3
3
|
describe ExcelUtils do
|
4
4
|
|
5
|
-
|
5
|
+
def assert_wrote(data, expected=data)
|
6
|
+
filename = tmp_path "#{SecureRandom.uuid}.xlsx"
|
6
7
|
|
7
|
-
|
8
|
+
ExcelUtils.write filename, data
|
9
|
+
|
10
|
+
workbook = ExcelUtils.read filename
|
11
|
+
|
12
|
+
expected = {'Sheet1' => expected} if expected.is_a?(Array)
|
13
|
+
|
14
|
+
workbook.to_h.must_equal expected
|
15
|
+
end
|
16
|
+
|
17
|
+
describe 'Single sheet' do
|
18
|
+
|
19
|
+
it 'Basic data' do
|
20
|
+
rows = [
|
21
|
+
{'column_a' => '1', 'column_b' => 'text 1'},
|
22
|
+
{'column_a' => '2.5', 'column_b' => nil, 'column_c' => 'text 2'},
|
23
|
+
]
|
24
|
+
|
25
|
+
assert_wrote rows, rows.map { |r| {'column_c' => nil}.merge r }
|
26
|
+
end
|
27
|
+
|
28
|
+
it 'Symbolized column names' do
|
29
|
+
rows = [
|
30
|
+
{column_a: 'text 1'},
|
31
|
+
{column_a: 'text 2'},
|
32
|
+
]
|
33
|
+
|
34
|
+
assert_wrote rows, rows.map { |r| {'column_a' => r[:column_a]} }
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'Long values' do
|
38
|
+
long_url = 'https://external.xx.fbcdn.net/safe_image.php?d=AQCY1f77h1RtuFfa&w=720&h=720&url=https%3A%2F%2Fwww.mercadolibre.com%2Fjms%2Fmla%2Flgz%2Fbackground%2Fsession%2Farmor.c27f26204365785956aaf9b30c289d0d4b0a201b4b01dfc3d83162b82608973be9a82ac85c2097c3f23b522681adca38b143f5e1a10fda251b430051a1592bf19caba204c07179120c48d47a1ceb5a45.774f4b8036d4b909101bab92870d262a%3Fbackground%3Darmor.c27f26204365785956aaf9b30c289d0d4b0a201b4b01dfc3d83162b82608973be9a82ac85c2097c3f23b522681adca38b143f5e1a10fda251b430051a1592bf19caba204c07179120c48d47a1ceb5a45.774f4b8036d4b909101bab92870d262a%26message%3DeyJqc190eXBlIjoianNfY29va2llIiwidmFsdWUiOiJ4In0&cfs=1&_nc_hash=AQB9Sf1HyWCwCVhF'
|
39
|
+
long_text = 'AQCY1f77h1RtuFfa&w=720&h=720&url=https%3A%2F%2Fwww.mercadolibre.com%2Fjms%2Fmla%2Flgz%2Fbackground%2Fsession%2Farmor.c27f26204365785956aaf9b30c289d0d4b0a201b4b01dfc3d83162b82608973be9a82ac85c2097c3f23b522681adca38b143f5e1a10fda251b430051a1592bf19caba204c07179120c48d47a1ceb5a45.774f4b8036d4b909101bab92870d262a%3Fbackground%3Darmor.c27f26204365785956aaf9b30c289d0d4b0a201b4b01dfc3d83162b82608973be9a82ac85c2097c3f23b522681adca38b143f5e1a10fda251b430051a1592bf19caba204c07179120c48d47a1ceb5a45.774f4b8036d4b909101bab92870d262a%26message%3DeyJqc190eXBlIjoianNfY29va2llIiwidmFsdWUiOiJ4In0&cfs=1&_nc_hash=AQB9Sf1HyWCwCVhF'
|
40
|
+
|
41
|
+
rows = [
|
42
|
+
{'column_a' => long_url},
|
43
|
+
{'column_a' => long_text},
|
44
|
+
]
|
45
|
+
|
46
|
+
assert_wrote rows
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'Numbers' do
|
50
|
+
rows = [
|
51
|
+
{'column_a' => 1},
|
52
|
+
{'column_a' => 2.5},
|
53
|
+
]
|
54
|
+
|
55
|
+
assert_wrote rows
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'Date and DateTime' do
|
59
|
+
rows = [
|
60
|
+
{'column_a' => DateTime.parse('2019-05-25T16:30:00')},
|
61
|
+
{'column_a' => Date.parse('2019-07-09')},
|
62
|
+
]
|
63
|
+
|
64
|
+
assert_wrote rows
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'Objects' do
|
68
|
+
rows = [
|
69
|
+
{'column_a' => ['text', 1]},
|
70
|
+
{'column_a' => {key: 123}},
|
71
|
+
{'column_a' => Set.new(['text', 1])}
|
72
|
+
]
|
73
|
+
|
74
|
+
assert_wrote rows, rows.map { |r| {'column_a' => r['column_a'].to_s} }
|
75
|
+
end
|
8
76
|
|
9
|
-
before do
|
10
|
-
FileUtils.rm_rf tmp_path
|
11
|
-
FileUtils.mkpath tmp_path
|
12
77
|
end
|
13
78
|
|
14
|
-
it '
|
79
|
+
it 'Multiple sheets' do
|
15
80
|
data = {
|
16
|
-
'
|
17
|
-
{'column_a' =>
|
18
|
-
{'
|
81
|
+
'strings' => [
|
82
|
+
{'column_a' => 'text 1', 'column_b' => 'text 2'},
|
83
|
+
{'column_a' => 'text 3', 'column_b' => 'text 4'}
|
19
84
|
],
|
20
|
-
'
|
21
|
-
{'
|
85
|
+
'numbers' => [
|
86
|
+
{'x' => 1, 'y' => 2},
|
87
|
+
{'x' => 3, 'y' => 4},
|
88
|
+
{'x' => 5, 'y' => 6}
|
22
89
|
],
|
23
|
-
'
|
90
|
+
'empty' => [],
|
24
91
|
}
|
25
92
|
|
26
|
-
|
27
|
-
|
28
|
-
workbook = ExcelUtils.read filename
|
29
|
-
|
30
|
-
workbook.to_h.must_equal data
|
93
|
+
assert_wrote data
|
31
94
|
end
|
32
95
|
|
33
96
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: excel_utils
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gabriel Naiman
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: inflecto
|
@@ -30,28 +30,28 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '2.
|
33
|
+
version: '2.8'
|
34
34
|
type: :runtime
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '2.
|
40
|
+
version: '2.8'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: roo-xls
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
44
44
|
requirements:
|
45
45
|
- - "~>"
|
46
46
|
- !ruby/object:Gem::Version
|
47
|
-
version: '1.
|
47
|
+
version: '1.2'
|
48
48
|
type: :runtime
|
49
49
|
prerelease: false
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
51
51
|
requirements:
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
|
-
version: '1.
|
54
|
+
version: '1.2'
|
55
55
|
- !ruby/object:Gem::Dependency
|
56
56
|
name: write_xlsx
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
@@ -66,6 +66,20 @@ dependencies:
|
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '0.85'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: nesquikcsv
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0.1'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0.1'
|
69
83
|
- !ruby/object:Gem::Dependency
|
70
84
|
name: rake
|
71
85
|
requirement: !ruby/object:Gem::Requirement
|
@@ -188,15 +202,26 @@ files:
|
|
188
202
|
- Rakefile
|
189
203
|
- excel_utils.gemspec
|
190
204
|
- lib/excel_utils.rb
|
191
|
-
- lib/excel_utils/
|
205
|
+
- lib/excel_utils/sheets/base.rb
|
206
|
+
- lib/excel_utils/sheets/csv.rb
|
207
|
+
- lib/excel_utils/sheets/excel.rb
|
208
|
+
- lib/excel_utils/sheets/excel_stream.rb
|
192
209
|
- lib/excel_utils/version.rb
|
193
|
-
- lib/excel_utils/
|
210
|
+
- lib/excel_utils/workbooks/csv.rb
|
211
|
+
- lib/excel_utils/workbooks/excel.rb
|
194
212
|
- lib/excel_utils/writer.rb
|
195
213
|
- spec/coverage_helper.rb
|
196
214
|
- spec/minitest_helper.rb
|
197
215
|
- spec/read_spec.rb
|
198
|
-
- spec/
|
199
|
-
- spec/
|
216
|
+
- spec/resources/basic.csv
|
217
|
+
- spec/resources/basic.ods
|
218
|
+
- spec/resources/basic.xls
|
219
|
+
- spec/resources/basic.xlsm
|
220
|
+
- spec/resources/basic.xlsx
|
221
|
+
- spec/resources/custom_extension.tmp
|
222
|
+
- spec/resources/empty.csv
|
223
|
+
- spec/resources/multiple.xlsx
|
224
|
+
- spec/resources/only_headers.csv
|
200
225
|
- spec/write_spec.rb
|
201
226
|
homepage: https://github.com/gabynaiman/excel_utils
|
202
227
|
licenses:
|
@@ -225,6 +250,13 @@ test_files:
|
|
225
250
|
- spec/coverage_helper.rb
|
226
251
|
- spec/minitest_helper.rb
|
227
252
|
- spec/read_spec.rb
|
228
|
-
- spec/
|
229
|
-
- spec/
|
253
|
+
- spec/resources/basic.csv
|
254
|
+
- spec/resources/basic.ods
|
255
|
+
- spec/resources/basic.xls
|
256
|
+
- spec/resources/basic.xlsm
|
257
|
+
- spec/resources/basic.xlsx
|
258
|
+
- spec/resources/custom_extension.tmp
|
259
|
+
- spec/resources/empty.csv
|
260
|
+
- spec/resources/multiple.xlsx
|
261
|
+
- spec/resources/only_headers.csv
|
230
262
|
- spec/write_spec.rb
|
data/lib/excel_utils/sheet.rb
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
module ExcelUtils
|
2
|
-
class Sheet
|
3
|
-
|
4
|
-
include Enumerable
|
5
|
-
|
6
|
-
attr_reader :name, :normalize_column_names
|
7
|
-
|
8
|
-
def initialize(name, spreadsheet, normalize_column_names: false)
|
9
|
-
@name = name
|
10
|
-
@spreadsheet = spreadsheet
|
11
|
-
@normalize_column_names = normalize_column_names
|
12
|
-
end
|
13
|
-
|
14
|
-
def column_names
|
15
|
-
@column_names ||= begin
|
16
|
-
if sheet.first_row
|
17
|
-
first_row = sheet.row sheet.first_row
|
18
|
-
normalize_column_names ? first_row.map { |n| Inflecto.underscore(n.strip.gsub(' ', '_')).to_sym } : first_row
|
19
|
-
else
|
20
|
-
[]
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|
24
|
-
|
25
|
-
def each(&block)
|
26
|
-
rows.each(&block)
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
attr_reader :spreadsheet
|
32
|
-
|
33
|
-
def sheet
|
34
|
-
spreadsheet.sheet name
|
35
|
-
end
|
36
|
-
|
37
|
-
def rows
|
38
|
-
@rows ||= begin
|
39
|
-
if sheet.first_row
|
40
|
-
sheet.to_a[1..-1].map do |row|
|
41
|
-
Hash[column_names.zip(row)]
|
42
|
-
end
|
43
|
-
else
|
44
|
-
[]
|
45
|
-
end
|
46
|
-
end
|
47
|
-
end
|
48
|
-
|
49
|
-
end
|
50
|
-
end
|
data/lib/excel_utils/workbook.rb
DELETED
@@ -1,33 +0,0 @@
|
|
1
|
-
module ExcelUtils
|
2
|
-
class Workbook
|
3
|
-
|
4
|
-
attr_reader :filename, :normalize_column_names
|
5
|
-
|
6
|
-
def initialize(filename, normalize_column_names: false)
|
7
|
-
@filename = filename
|
8
|
-
@normalize_column_names = normalize_column_names
|
9
|
-
@spreadsheet = Roo::Spreadsheet.open filename
|
10
|
-
end
|
11
|
-
|
12
|
-
def sheets
|
13
|
-
@sheets ||= spreadsheet.sheets.map do |name|
|
14
|
-
Sheet.new name, spreadsheet, normalize_column_names: normalize_column_names
|
15
|
-
end
|
16
|
-
end
|
17
|
-
|
18
|
-
def [](sheet_name)
|
19
|
-
sheets.detect { |sheet| sheet.name == sheet_name }
|
20
|
-
end
|
21
|
-
|
22
|
-
def to_h
|
23
|
-
sheets.each_with_object({}) do |sheet, hash|
|
24
|
-
hash[sheet.name] = sheet.to_a
|
25
|
-
end
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
attr_reader :spreadsheet
|
31
|
-
|
32
|
-
end
|
33
|
-
end
|
data/spec/sample.xlsx
DELETED
Binary file
|