excel-esv 3.0.1 → 3.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 61b9d8716af48b7a32382ba6e9558a5f9f2afc71cb6ec0e6a380686dd91ea308
4
- data.tar.gz: 8d737947eb7b20510cd234a5c23a033c5eaec2a8f90d22152c8c6892de99e271
3
+ metadata.gz: bb907e9f6b481001f01561df298d6052f3efb37f4ae3f57191b2bb807699203e
4
+ data.tar.gz: ea18830fa510acd5fe6d204714fbfc75114c0e167d08bd3fa969b61406893bf2
5
5
  SHA512:
6
- metadata.gz: e72fa2c2685e9833c15d4ea0c6278b41ca0b8f45fe296ff81f6691652cd0b477f68e69365b9bc42b3d70024738361cd8805ab0d21885de875b028febab293d0d
7
- data.tar.gz: 2dacf749dcc74944f9ad3cf2ef671e123a16f8665ecfab299541cd22c702663b9e168218675cfd75cc56ef1bac455b3d90f6f554f0d3549f71928931074808ea
6
+ metadata.gz: 745103dd2cbf9eda28e2d2947f4785b6a3f95372be9c3044265029cb4ac2e01c16d943923cb6e3f677387b2dfc6b5cbaba6bfddab001c0e775c97a92e967870b
7
+ data.tar.gz: e22bb76f26d291021847496fd679f6f982c4ecd14f0e9978674f08607f90d848b9539d5931d0a1ae038af8c22a8254814e69198af1802f755960e2fc0394ece4
@@ -0,0 +1,6 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "github-actions"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "weekly"
@@ -7,16 +7,22 @@ on:
7
7
  branches: [ master ]
8
8
 
9
9
  jobs:
10
- test:
10
+ ruby-versions:
11
+ uses: ruby/actions/.github/workflows/ruby_versions.yml@master
12
+ with:
13
+ min_version: 3.2
14
+ engine: cruby
11
15
 
16
+ test:
17
+ needs: ruby-versions
12
18
  runs-on: ubuntu-latest
13
19
 
14
20
  strategy:
15
21
  matrix:
16
- ruby-version: ["3.0", "2.7", "2.6", "2.5"]
17
-
22
+ ruby-version: ${{ fromJson(needs.ruby-versions.outputs.versions) }}
23
+
18
24
  steps:
19
- - uses: actions/checkout@v2
25
+ - uses: actions/checkout@v5
20
26
  - name: Set up Ruby ${{ matrix.ruby-version }}
21
27
  uses: ruby/setup-ruby@v1
22
28
  with:
data/.gitignore CHANGED
@@ -11,4 +11,5 @@
11
11
  *.so
12
12
  *.o
13
13
  *.a
14
+ *.gem
14
15
  mkmf.log
data/CHANGELOG.md CHANGED
@@ -1,12 +1,24 @@
1
- # 3.0.0
1
+ # Changelog
2
+
3
+ ## 3.1.0 (Nov 12, 2025)
4
+
5
+ * Support specifying worksheet index when there are multiple: `ESV.parse(data, worksheet_index: 0)`.
6
+
7
+ ## 3.0.1 (Nov 19, 2021)
8
+
9
+ * Add `ESV.parse(data, header_converters:)` [#9]
10
+
11
+ [#9]: https://github.com/barsoom/excel-esv/pull/9
12
+
13
+ ## 3.0.0 (Nov 20, 2018)
2
14
 
3
15
  * Returns the last value of any formula cells instead of returning a `Spreadsheet::Formula`.
4
16
  * Returns the URL of any link cells instead of returning a `Spreadsheet::Link`.
5
17
 
6
- # 2.0.0
18
+ ## 2.0.0 (Nov 19, 2018)
7
19
 
8
20
  * `parse` now returns an actual nested `Array`, not array-like `Spreadsheet::Row` records.
9
21
 
10
- # 1.0.0
22
+ ## 1.0.0 (Dec 8, 2017)
11
23
 
12
24
  * `send_excel` now supports a `filename:` argument, e.g. `send_excel(data, filename: "salaries.xls")`.
data/Gemfile CHANGED
@@ -4,7 +4,7 @@ source "https://rubygems.org"
4
4
  gemspec
5
5
 
6
6
  group :development do
7
- gem "barsoom_utils", github: "barsoom/barsoom_utils"
7
+ gem "barsoom_utils"
8
8
  gem "bundler"
9
9
  gem "rake"
10
10
  gem "rspec"
data/README.md CHANGED
@@ -45,7 +45,31 @@ output = ESV.parse(data)
45
45
  # => [ [ "Name", "Dogs", … ], … ]
46
46
  ```
47
47
 
48
- This assumes a file with a single worksheet and will raise otherwise.
48
+ This will raise for a file with multiple worksheets unless you explicitly specify the one you want (the first worksheet is index 0):
49
+
50
+ ``` ruby
51
+ ESV.parse(data, worksheet_index: 0)
52
+ ```
53
+
54
+ `.parse` supports the `header_converters:` keyword argument, which takes the same arguments as `CSV.parse` does:
55
+
56
+ - a Symbol name for a registered header converter
57
+ - a Proc which takes the value and returns the converted value
58
+ - an Array of Symbol names for registered header converters
59
+
60
+ ``` ruby
61
+ require "esv"
62
+
63
+ data = File.read("/tmp/test.xls")
64
+ output = ESV.parse(data, header_converters: :symbol)
65
+ # => [ [ :name, :dogs, … ], … ]
66
+ ```
67
+
68
+ Registering a new converter:
69
+
70
+ ``` ruby
71
+ ESV::HEADER_CONVERTERS[:upcase] = ->(value) { value.upcase }
72
+ ```
49
73
 
50
74
  ### Parse file
51
75
 
@@ -58,6 +82,8 @@ output = ESV.parse_file("/tmp/test.xls")
58
82
 
59
83
  This assumes a file with a single worksheet and will raise otherwise.
60
84
 
85
+ Also supports `header_converters:`.
86
+
61
87
  ### Generate in Ruby on Rails
62
88
 
63
89
  In `config/initializers/mime_types.rb`:
data/excel-esv.gemspec CHANGED
@@ -16,5 +16,6 @@ Gem::Specification.new do |spec|
16
16
  spec.files = `git ls-files -z`.split("\x0")
17
17
  spec.require_paths = [ "lib" ]
18
18
 
19
- spec.add_dependency "spreadsheet"
19
+ spec.add_runtime_dependency "spreadsheet"
20
+ spec.add_runtime_dependency "logger"
20
21
  end
data/lib/esv/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module ESV
2
- VERSION = "3.0.1"
2
+ VERSION = "3.1.0"
3
3
  end
data/lib/esv.rb CHANGED
@@ -16,27 +16,78 @@ module ESV
16
16
  end
17
17
  end
18
18
 
19
- def self.parse(data)
19
+ # You can register your own header converters in this Hash.
20
+ #
21
+ # @see https://rubyapi.org/3.2/o/csv#class-CSV-label-Custom+Header+Converters
22
+ HEADER_CONVERTERS = {
23
+ downcase: ->(value) {
24
+ value.respond_to?(:downcase) ? value.downcase : value
25
+ },
26
+ # Details:
27
+ #
28
+ # Strips leading and trailing whitespace.
29
+ # Downcases the header.
30
+ # Replaces embedded spaces with underscores.
31
+ # Removes non-word characters.
32
+ # Makes the string into a Symbol.
33
+ symbol: ->(value) {
34
+ value.to_s.strip.downcase.tr(" ", "_").gsub(/\W+/, "").to_sym
35
+ },
36
+ }
37
+
38
+ # @param data [Object] Spreadsheet data to be parsed.
39
+ # @param header_converters [nil, Symbol, Proc, Array<Symbol>] If given, can take these forms:
40
+ # - a Symbol name for a registered header converter
41
+ # - a Proc which takes the value and returns the converted value
42
+ # - an Array of Symbol names for registered header converters
43
+ # @param worksheet_index [nil, Integer] If given, specifies which worksheet index to parse (first is 0). If nil, expects exactly one worksheet.
44
+ # @return [Array<Array>] a list of rows
45
+ def self.parse(data, header_converters: nil, worksheet_index: nil)
20
46
  fake_file = StringIO.new(data)
21
47
  book = Spreadsheet.open(fake_file)
22
48
 
23
- # We could support multiple worksheets, but let's not until we actually need it.
24
- # Until then, we prefer raising to silently ignoring worksheets.
25
- worksheet_count = book.worksheets.length
26
- raise "Expected 1 worksheet, found #{worksheet_count}." if worksheet_count > 1
27
-
28
- book.worksheet(0).to_a.map(&:to_a).map { |row|
29
- row.map { |cell|
30
- case cell
31
- when Spreadsheet::Formula then cell.value
32
- when Spreadsheet::Link then cell.href
33
- else cell
49
+ # We prefer raising to silently ignoring worksheets.
50
+ if !worksheet_index && book.worksheets.length > 1
51
+ raise "Expected 1 worksheet, found #{book.worksheets.length}."
52
+ end
53
+
54
+ worksheet_index ||= 0
55
+
56
+ is_first_row = true
57
+ book.worksheet(worksheet_index).to_a.map(&:to_a).map { |row|
58
+ row.each_with_index.map { |cell, index|
59
+ value =
60
+ case cell
61
+ when Spreadsheet::Formula then cell.value
62
+ when Spreadsheet::Link then cell.href
63
+ else cell
64
+ end
65
+
66
+ if header_converters && is_first_row
67
+ case header_converters
68
+ when Proc then
69
+ value = header_converters.call(value)
70
+ when Symbol then
71
+ value = HEADER_CONVERTERS[header_converters].call(value)
72
+ when Enumerable then
73
+ # Apply the converters in order.
74
+ header_converters.each { |name|
75
+ value = HEADER_CONVERTERS.fetch(name).call(value)
76
+ }
77
+ else
78
+ raise "Unsupported kind of header_converters #{header_converters.inspect}"
79
+ end
80
+ value
81
+ else
82
+ value
34
83
  end
84
+ }.tap {
85
+ is_first_row = false
35
86
  }
36
87
  }
37
88
  end
38
89
 
39
- def self.parse_file(path)
40
- parse File.read(path)
90
+ def self.parse_file(path, header_converters: nil)
91
+ parse(File.read(path), header_converters: header_converters)
41
92
  end
42
93
  end
data/spec/esv_spec.rb CHANGED
@@ -1,114 +1,224 @@
1
1
  require "esv"
2
2
 
3
- RSpec.describe ESV, ".generate and .parse" do
4
- it "works" do
5
- data = ESV.generate do |esv|
6
- esv << [ "Dogs", "Cats" ]
7
- esv << [ 1, 2 ]
3
+ RSpec.describe ESV do
4
+ describe ".generate and .parse" do
5
+ it "works" do
6
+ data = ESV.generate do |esv|
7
+ esv << [ "Dogs", "Cats" ]
8
+ esv << [ 1, 2 ]
9
+ end
10
+
11
+ output = ESV.parse(data)
12
+
13
+ expect(output).to eq [
14
+ [ "Dogs", "Cats" ],
15
+ [ 1, 2 ],
16
+ ]
8
17
  end
9
-
10
- output = ESV.parse(data)
11
-
12
- expect(output).to eq [
13
- [ "Dogs", "Cats" ],
14
- [ 1, 2 ],
15
- ]
16
18
  end
17
- end
18
19
 
19
- RSpec.describe ESV, ".parse" do
20
- it "raises if there's more than one worksheet" do
21
- excel_file_with_two_worksheets = generate_excel_file do |sheet, book|
22
- book.create_worksheet
20
+ describe "::HEADER_CONVERTERS" do
21
+ context "with :downcase" do
22
+ it "downcases string value" do
23
+ expect(described_class::HEADER_CONVERTERS[:downcase].call("CAT")).to eq("cat")
24
+ end
25
+
26
+ it "returns a non-string value" do
27
+ expect(described_class::HEADER_CONVERTERS[:downcase].call(1)).to eq(1)
28
+ end
23
29
  end
24
30
 
25
- expect {
26
- ESV.parse(excel_file_with_two_worksheets)
27
- }.to raise_error(/Expected 1 worksheet, found 2/)
31
+ context "with :symbol" do
32
+ it "stringifies before symbolizing given value" do
33
+ expect(described_class::HEADER_CONVERTERS[:symbol].call(1)).to eq(:"1")
34
+ end
35
+
36
+ it "symbolizes given value" do
37
+ expect(described_class::HEADER_CONVERTERS[:symbol].call("cat")).to eq(:cat)
38
+ end
39
+
40
+ context "symbolizes and" do
41
+ it "downcases given value" do
42
+ expect(described_class::HEADER_CONVERTERS[:symbol].call("CAT")).to eq(:cat)
43
+ end
44
+
45
+ it "strips any spaces around value" do
46
+ expect(described_class::HEADER_CONVERTERS[:symbol].call(" CAT ")).to eq(:cat)
47
+ end
48
+
49
+ it "replaces embedded spaces with underscores" do
50
+ expect(described_class::HEADER_CONVERTERS[:symbol].call("CAT CAT")).to eq(:cat_cat)
51
+ end
52
+
53
+ it "removes non-word characters" do
54
+ expect(described_class::HEADER_CONVERTERS[:symbol].call("CAT?")).to eq(:cat)
55
+ end
56
+ end
57
+ end
28
58
  end
29
59
 
30
- it "ignores formatting, always returning a plain array of data" do
31
- excel_file_with_formatting = generate_excel_file do |sheet|
32
- sheet.row(0).replace([ 1, 2 ])
33
- sheet.row(0).default_format = Spreadsheet::Format.new(color: :blue)
60
+ describe ".parse" do
61
+ describe "worksheet handling" do
62
+ context "with more than one worksheet" do
63
+ let(:excel_file_with_two_worksheets) {
64
+ generate_excel_file do |sheet_0, book|
65
+ sheet_0.row(0).replace([ "I am worksheet 0" ])
66
+
67
+ sheet_1 = book.create_worksheet
68
+ sheet_1.row(0).replace([ "I am worksheet 1" ])
69
+ end
70
+ }
71
+
72
+ it "raises when not given a worksheet index" do
73
+ expect {
74
+ ESV.parse(excel_file_with_two_worksheets)
75
+ }.to raise_error(/Expected 1 worksheet, found 2/)
76
+ end
77
+
78
+ it "allows specifying a worksheet index" do
79
+ output = ESV.parse(excel_file_with_two_worksheets, worksheet_index: 0)
80
+ expect(output).to eq [[ "I am worksheet 0" ]]
81
+
82
+ output = ESV.parse(excel_file_with_two_worksheets, worksheet_index: 1)
83
+ expect(output).to eq [[ "I am worksheet 1" ]]
84
+ end
85
+ end
34
86
  end
35
87
 
36
- output = ESV.parse(excel_file_with_formatting)
88
+ it "ignores formatting, always returning a plain array of data" do
89
+ excel_file_with_formatting = generate_excel_file do |sheet|
90
+ sheet.row(0).replace([ 1, 2 ])
91
+ sheet.row(0).default_format = Spreadsheet::Format.new(color: :blue)
92
+ end
37
93
 
38
- expect(output).to eq [
39
- [ 1, 2 ],
40
- ]
94
+ output = ESV.parse(excel_file_with_formatting)
41
95
 
42
- expect(output[0].class).to eq Array
43
- end
96
+ expect(output).to eq [
97
+ [ 1, 2 ],
98
+ ]
44
99
 
45
- it "returns the last value of a formula cell" do
46
- excel_file_with_formula = generate_excel_file do |sheet|
47
- formula = Spreadsheet::Formula.new
48
- formula.value = "two"
49
- sheet.row(0).replace([ "one", formula ])
100
+ expect(output[0].class).to eq Array
50
101
  end
51
102
 
52
- output = ESV.parse(excel_file_with_formula)
103
+ it "returns the last value of a formula cell" do
104
+ excel_file_with_formula = generate_excel_file do |sheet|
105
+ formula = Spreadsheet::Formula.new
106
+ formula.value = "two"
107
+ sheet.row(0).replace([ "one", formula ])
108
+ end
53
109
 
54
- expect(output).to eq [
55
- [ "one", "two" ],
56
- ]
57
- expect(output[0].class).to eq Array
58
- end
110
+ output = ESV.parse(excel_file_with_formula)
59
111
 
112
+ expect(output).to eq [
113
+ [ "one", "two" ],
114
+ ]
115
+ expect(output[0].class).to eq Array
116
+ end
60
117
 
61
- it "returns the URL of a link cell" do
62
- excel_file_with_link = generate_excel_file do |sheet|
63
- link = Spreadsheet::Link.new("https://example.com", "desc", "foo")
64
- sheet.row(0).replace([ "one", link ])
118
+
119
+ it "returns the URL of a link cell" do
120
+ excel_file_with_link = generate_excel_file do |sheet|
121
+ link = Spreadsheet::Link.new("https://example.com", "desc", "foo")
122
+ sheet.row(0).replace([ "one", link ])
123
+ end
124
+ output = ESV.parse(excel_file_with_link)
125
+ expect(output).to eq [
126
+ [ "one", "https://example.com#foo" ],
127
+ ]
128
+
129
+ expect(output[0][1].class).to eq String
65
130
  end
66
- output = ESV.parse(excel_file_with_link)
67
- expect(output).to eq [
68
- [ "one", "https://example.com#foo" ],
69
- ]
70
131
 
71
- expect(output[0][1].class).to eq String
72
- end
132
+ context "when given a header_converters: option" do
133
+ let(:data) {
134
+ ESV.generate do |esv|
135
+ esv << [ "Dogs", "Cats cats" ]
136
+ esv << [ 1, 2 ]
137
+ end
138
+ }
139
+
140
+ it "lets you specify header_converters by symbolic name" do
141
+ output = ESV.parse(data, header_converters: :downcase)
142
+
143
+ expect(output).to eq [
144
+ [ "dogs", "cats cats" ],
145
+ [ 1, 2 ],
146
+ ]
147
+ end
148
+
149
+ it "lets you specify header_converters as a proc" do
150
+ output = ESV.parse(data, header_converters: ->(value) { value.upcase })
151
+
152
+ expect(output).to eq [
153
+ [ "DOGS", "CATS CATS" ],
154
+ [ 1, 2 ],
155
+ ]
156
+ end
157
+
158
+ it "lets you register new symbolic names for header_converters and use them" do
159
+ ESV::HEADER_CONVERTERS[:upcase] = ->(value) { value.upcase }
160
+
161
+ output = ESV.parse(data, header_converters: :upcase)
162
+
163
+ expect(output).to eq [
164
+ [ "DOGS", "CATS CATS" ],
165
+ [ 1, 2 ],
166
+ ]
167
+
168
+ ESV::HEADER_CONVERTERS.delete(:upcase)
169
+ end
170
+
171
+ it "lets you specify header_converters by symbolic names as a list and have them apply in order" do
172
+ ESV::HEADER_CONVERTERS[:reverse] = ->(value) { value.reverse }
173
+ output = ESV.parse(data, header_converters: [ :downcase, :reverse, :symbol ])
174
+
175
+ expect(output).to eq [
176
+ [ :sgod, :stac_stac ],
177
+ [ 1, 2 ],
178
+ ]
179
+ ESV::HEADER_CONVERTERS.delete(:reverse)
180
+ end
181
+ end
73
182
 
74
- private
183
+ private
75
184
 
76
- def generate_excel_file(&block)
77
- book = Spreadsheet::Workbook.new
78
- sheet = book.create_worksheet
185
+ def generate_excel_file(&block)
186
+ book = Spreadsheet::Workbook.new
187
+ sheet = book.create_worksheet
79
188
 
80
- block.call(sheet, book)
189
+ block.call(sheet, book)
81
190
 
82
- data = ""
83
- fake_file = StringIO.new(data)
84
- book.write(fake_file)
85
- data
191
+ data = ""
192
+ fake_file = StringIO.new(data)
193
+ book.write(fake_file)
194
+ data
195
+ end
86
196
  end
87
- end
88
197
 
89
- RSpec.describe ESV, ".generate_file and .parse_file" do
90
- before do
91
- @file = Tempfile.new("esv")
92
- end
198
+ describe ".generate_file and .parse_file" do
199
+ before do
200
+ @file = Tempfile.new("esv")
201
+ end
93
202
 
94
- it "works" do
95
- path = @file.path
203
+ it "works" do
204
+ path = @file.path
96
205
 
97
- ESV.generate_file(path) do |esv|
98
- esv << [ "Dogs", "Cats" ]
99
- esv << [ 1, 2 ]
100
- end
206
+ ESV.generate_file(path) do |esv|
207
+ esv << [ "Dogs", "Cats" ]
208
+ esv << [ 1, 2 ]
209
+ end
101
210
 
102
- output = ESV.parse_file(path)
211
+ output = ESV.parse_file(path)
103
212
 
104
- expect(output).to eq [
105
- [ "Dogs", "Cats" ],
106
- [ 1, 2 ],
107
- ]
108
- end
213
+ expect(output).to eq [
214
+ [ "Dogs", "Cats" ],
215
+ [ 1, 2 ],
216
+ ]
217
+ end
109
218
 
110
- after do
111
- @file.close
112
- @file.unlink
219
+ after do
220
+ @file.close
221
+ @file.unlink
222
+ end
113
223
  end
114
224
  end
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: excel-esv
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.1
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Henrik Nyh
8
- autorequire:
9
8
  bindir: bin
10
9
  cert_chain: []
11
- date: 2021-11-19 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: spreadsheet
@@ -24,13 +23,27 @@ dependencies:
24
23
  - - ">="
25
24
  - !ruby/object:Gem::Version
26
25
  version: '0'
27
- description:
26
+ - !ruby/object:Gem::Dependency
27
+ name: logger
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
28
40
  email:
29
41
  - henrik@nyh.se
30
42
  executables: []
31
43
  extensions: []
32
44
  extra_rdoc_files: []
33
45
  files:
46
+ - ".github/dependabot.yml"
34
47
  - ".github/workflows/ci.yml"
35
48
  - ".gitignore"
36
49
  - ".rspec"
@@ -53,7 +66,6 @@ licenses:
53
66
  - MIT
54
67
  metadata:
55
68
  rubygems_mfa_required: 'true'
56
- post_install_message:
57
69
  rdoc_options: []
58
70
  require_paths:
59
71
  - lib
@@ -68,8 +80,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
80
  - !ruby/object:Gem::Version
69
81
  version: '0'
70
82
  requirements: []
71
- rubygems_version: 3.2.28
72
- signing_key:
83
+ rubygems_version: 3.6.9
73
84
  specification_version: 4
74
85
  summary: Excel parsing and generation with the ease of CSV.
75
86
  test_files: []