roo 2.6.0 → 2.8.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (86) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +17 -0
  3. data/.github/issue_template.md +16 -0
  4. data/.github/pull_request_template.md +14 -0
  5. data/.rubocop.yml +186 -0
  6. data/.travis.yml +14 -11
  7. data/CHANGELOG.md +64 -2
  8. data/Gemfile +2 -4
  9. data/LICENSE +2 -0
  10. data/README.md +36 -10
  11. data/lib/roo/base.rb +82 -225
  12. data/lib/roo/constants.rb +5 -3
  13. data/lib/roo/csv.rb +100 -97
  14. data/lib/roo/excelx/cell/base.rb +26 -12
  15. data/lib/roo/excelx/cell/boolean.rb +9 -6
  16. data/lib/roo/excelx/cell/date.rb +7 -7
  17. data/lib/roo/excelx/cell/datetime.rb +50 -44
  18. data/lib/roo/excelx/cell/empty.rb +3 -2
  19. data/lib/roo/excelx/cell/number.rb +44 -47
  20. data/lib/roo/excelx/cell/string.rb +3 -3
  21. data/lib/roo/excelx/cell/time.rb +17 -16
  22. data/lib/roo/excelx/cell.rb +10 -6
  23. data/lib/roo/excelx/comments.rb +3 -3
  24. data/lib/roo/excelx/coordinate.rb +11 -4
  25. data/lib/roo/excelx/extractor.rb +21 -3
  26. data/lib/roo/excelx/format.rb +38 -31
  27. data/lib/roo/excelx/images.rb +26 -0
  28. data/lib/roo/excelx/relationships.rb +12 -4
  29. data/lib/roo/excelx/shared.rb +10 -3
  30. data/lib/roo/excelx/shared_strings.rb +9 -15
  31. data/lib/roo/excelx/sheet.rb +49 -10
  32. data/lib/roo/excelx/sheet_doc.rb +89 -48
  33. data/lib/roo/excelx/styles.rb +3 -3
  34. data/lib/roo/excelx/workbook.rb +7 -3
  35. data/lib/roo/excelx.rb +50 -19
  36. data/lib/roo/formatters/base.rb +15 -0
  37. data/lib/roo/formatters/csv.rb +84 -0
  38. data/lib/roo/formatters/matrix.rb +23 -0
  39. data/lib/roo/formatters/xml.rb +31 -0
  40. data/lib/roo/formatters/yaml.rb +40 -0
  41. data/lib/roo/helpers/default_attr_reader.rb +20 -0
  42. data/lib/roo/helpers/weak_instance_cache.rb +41 -0
  43. data/lib/roo/open_office.rb +17 -9
  44. data/lib/roo/spreadsheet.rb +1 -1
  45. data/lib/roo/tempdir.rb +5 -10
  46. data/lib/roo/utils.rb +70 -20
  47. data/lib/roo/version.rb +1 -1
  48. data/lib/roo.rb +4 -1
  49. data/roo.gemspec +14 -11
  50. data/spec/lib/roo/base_spec.rb +45 -3
  51. data/spec/lib/roo/excelx/relationships_spec.rb +43 -0
  52. data/spec/lib/roo/excelx/sheet_doc_spec.rb +11 -0
  53. data/spec/lib/roo/excelx_spec.rb +150 -31
  54. data/spec/lib/roo/strict_spec.rb +43 -0
  55. data/spec/lib/roo/utils_spec.rb +25 -3
  56. data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
  57. data/spec/lib/roo_spec.rb +0 -0
  58. data/spec/spec_helper.rb +2 -6
  59. data/test/excelx/cell/test_attr_reader_default.rb +72 -0
  60. data/test/excelx/cell/test_base.rb +5 -0
  61. data/test/excelx/cell/test_datetime.rb +6 -6
  62. data/test/excelx/cell/test_empty.rb +11 -0
  63. data/test/excelx/cell/test_number.rb +9 -0
  64. data/test/excelx/cell/test_string.rb +20 -0
  65. data/test/excelx/cell/test_time.rb +5 -5
  66. data/test/excelx/test_coordinate.rb +51 -0
  67. data/test/formatters/test_csv.rb +136 -0
  68. data/test/formatters/test_matrix.rb +76 -0
  69. data/test/formatters/test_xml.rb +78 -0
  70. data/test/formatters/test_yaml.rb +20 -0
  71. data/test/helpers/test_accessing_files.rb +60 -0
  72. data/test/helpers/test_comments.rb +43 -0
  73. data/test/helpers/test_formulas.rb +9 -0
  74. data/test/helpers/test_labels.rb +103 -0
  75. data/test/helpers/test_sheets.rb +55 -0
  76. data/test/helpers/test_styles.rb +62 -0
  77. data/test/roo/test_base.rb +182 -0
  78. data/test/roo/test_csv.rb +88 -0
  79. data/test/roo/test_excelx.rb +330 -0
  80. data/test/roo/test_libre_office.rb +9 -0
  81. data/test/roo/test_open_office.rb +289 -0
  82. data/test/test_helper.rb +129 -14
  83. data/test/test_roo.rb +32 -1787
  84. metadata +81 -29
  85. data/.github/ISSUE_TEMPLATE +0 -10
  86. data/Gemfile_ruby2 +0 -29
@@ -2,12 +2,12 @@ module Roo
2
2
  class Excelx
3
3
  class Cell
4
4
  class String < Cell::Base
5
- attr_reader :value, :formula, :format, :cell_type, :cell_value, :link, :coordinate
5
+ attr_reader :value, :formula, :format, :cell_value, :coordinate
6
+
7
+ attr_reader_with_default default_type: :string, cell_type: :string
6
8
 
7
9
  def initialize(value, formula, style, link, coordinate)
8
10
  super(value, formula, nil, style, link, coordinate)
9
- @type = @cell_type = :string
10
- @value = link? ? Roo::Link.new(link, value) : value
11
11
  end
12
12
 
13
13
  def empty?
@@ -4,15 +4,16 @@ module Roo
4
4
  class Excelx
5
5
  class Cell
6
6
  class Time < Roo::Excelx::Cell::DateTime
7
- attr_reader :value, :formula, :format, :cell_value, :link, :coordinate
7
+ attr_reader :value, :formula, :format, :cell_value, :coordinate
8
+
9
+ attr_reader_with_default default_type: :time
8
10
 
9
11
  def initialize(value, formula, excelx_type, style, link, base_date, coordinate)
10
12
  # NOTE: Pass all arguments to DateTime super class.
11
13
  super
12
- @type = :time
13
14
  @format = excelx_type.last
14
15
  @datetime = create_datetime(base_date, value)
15
- @value = link? ? Roo::Link.new(link, value) : (value.to_f * 86_400).to_i
16
+ @value = link ? Roo::Link.new(link, value) : (value.to_f * 86_400).to_i
16
17
  end
17
18
 
18
19
  def formatted_value
@@ -24,19 +25,19 @@ module Roo
24
25
 
25
26
  private
26
27
 
27
- def create_datetime(base_date, value)
28
- date = base_date + value.to_f.round(6)
29
- datetime_string = date.strftime('%Y-%m-%d %H:%M:%S.%N')
30
- t = round_datetime(datetime_string)
31
-
32
- ::DateTime.civil(t.year, t.month, t.day, t.hour, t.min, t.sec)
33
- end
34
-
35
- def round_datetime(datetime_string)
36
- /(?<yyyy>\d+)-(?<mm>\d+)-(?<dd>\d+) (?<hh>\d+):(?<mi>\d+):(?<ss>\d+.\d+)/ =~ datetime_string
37
-
38
- ::Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0)
39
- end
28
+ # def create_datetime(base_date, value)
29
+ # date = base_date + value.to_f.round(6)
30
+ # datetime_string = date.strftime('%Y-%m-%d %H:%M:%S.%N')
31
+ # t = round_datetime(datetime_string)
32
+ #
33
+ # ::DateTime.civil(t.year, t.month, t.day, t.hour, t.min, t.sec)
34
+ # end
35
+
36
+ # def round_datetime(datetime_string)
37
+ # /(?<yyyy>\d+)-(?<mm>\d+)-(?<dd>\d+) (?<hh>\d+):(?<mi>\d+):(?<ss>\d+.\d+)/ =~ datetime_string
38
+ #
39
+ # ::Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0)
40
+ # end
40
41
  end
41
42
  end
42
43
  end
@@ -40,19 +40,23 @@ module Roo
40
40
  end
41
41
 
42
42
  def self.create_cell(type, *values)
43
+ cell_class(type)&.new(*values)
44
+ end
45
+
46
+ def self.cell_class(type)
43
47
  case type
44
48
  when :string
45
- Cell::String.new(*values)
49
+ Cell::String
46
50
  when :boolean
47
- Cell::Boolean.new(*values)
51
+ Cell::Boolean
48
52
  when :number
49
- Cell::Number.new(*values)
53
+ Cell::Number
50
54
  when :date
51
- Cell::Date.new(*values)
55
+ Cell::Date
52
56
  when :datetime
53
- Cell::DateTime.new(*values)
57
+ Cell::DateTime
54
58
  when :time
55
- Cell::Time.new(*values)
59
+ Cell::Time
56
60
  end
57
61
  end
58
62
 
@@ -12,10 +12,10 @@ module Roo
12
12
  def extract_comments
13
13
  return {} unless doc_exists?
14
14
 
15
- Hash[doc.xpath('//comments/commentList/comment').map do |comment|
15
+ doc.xpath('//comments/commentList/comment').each_with_object({}) do |comment, hash|
16
16
  value = (comment.at_xpath('./text/r/t') || comment.at_xpath('./text/t')).text
17
- [::Roo::Utils.ref_to_key(comment.attributes['ref'].to_s), value]
18
- end]
17
+ hash[::Roo::Utils.ref_to_key(comment['ref'].to_s)] = value
18
+ end
19
19
  end
20
20
  end
21
21
  end
@@ -1,11 +1,18 @@
1
1
  module Roo
2
2
  class Excelx
3
- class Coordinate
4
- attr_accessor :row, :column
3
+ class Coordinate < ::Array
5
4
 
6
5
  def initialize(row, column)
7
- @row = row
8
- @column = column
6
+ super() << row << column
7
+ freeze
8
+ end
9
+
10
+ def row
11
+ self[0]
12
+ end
13
+
14
+ def column
15
+ self[1]
9
16
  end
10
17
  end
11
18
  end
@@ -1,16 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roo/helpers/weak_instance_cache"
4
+
1
5
  module Roo
2
6
  class Excelx
3
7
  class Extractor
4
- def initialize(path)
8
+ include Roo::Helpers::WeakInstanceCache
9
+
10
+ COMMON_STRINGS = {
11
+ t: "t",
12
+ r: "r",
13
+ s: "s",
14
+ ref: "ref",
15
+ html_tag_open: "<html>",
16
+ html_tag_closed: "</html>"
17
+ }
18
+
19
+ def initialize(path, options = {})
5
20
  @path = path
21
+ @options = options
6
22
  end
7
23
 
8
24
  private
9
25
 
10
26
  def doc
11
- raise FileNotFound, "#{@path} file not found" unless doc_exists?
27
+ instance_cache(:@doc) do
28
+ raise FileNotFound, "#{@path} file not found" unless doc_exists?
12
29
 
13
- ::Roo::Utils.load_xml(@path).remove_namespaces!
30
+ ::Roo::Utils.load_xml(@path).remove_namespaces!
31
+ end
14
32
  end
15
33
 
16
34
  def doc_exists?
@@ -1,49 +1,57 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Roo
2
4
  class Excelx
3
5
  module Format
6
+ extend self
4
7
  EXCEPTIONAL_FORMATS = {
5
8
  'h:mm am/pm' => :date,
6
9
  'h:mm:ss am/pm' => :date
7
10
  }
8
11
 
9
12
  STANDARD_FORMATS = {
10
- 0 => 'General'.freeze,
11
- 1 => '0'.freeze,
12
- 2 => '0.00'.freeze,
13
- 3 => '#,##0'.freeze,
14
- 4 => '#,##0.00'.freeze,
15
- 9 => '0%'.freeze,
16
- 10 => '0.00%'.freeze,
17
- 11 => '0.00E+00'.freeze,
18
- 12 => '# ?/?'.freeze,
19
- 13 => '# ??/??'.freeze,
20
- 14 => 'mm-dd-yy'.freeze,
21
- 15 => 'd-mmm-yy'.freeze,
22
- 16 => 'd-mmm'.freeze,
23
- 17 => 'mmm-yy'.freeze,
24
- 18 => 'h:mm AM/PM'.freeze,
25
- 19 => 'h:mm:ss AM/PM'.freeze,
26
- 20 => 'h:mm'.freeze,
27
- 21 => 'h:mm:ss'.freeze,
28
- 22 => 'm/d/yy h:mm'.freeze,
29
- 37 => '#,##0 ;(#,##0)'.freeze,
30
- 38 => '#,##0 ;[Red](#,##0)'.freeze,
31
- 39 => '#,##0.00;(#,##0.00)'.freeze,
32
- 40 => '#,##0.00;[Red](#,##0.00)'.freeze,
33
- 45 => 'mm:ss'.freeze,
34
- 46 => '[h]:mm:ss'.freeze,
35
- 47 => 'mmss.0'.freeze,
36
- 48 => '##0.0E+0'.freeze,
37
- 49 => '@'.freeze
13
+ 0 => 'General',
14
+ 1 => '0',
15
+ 2 => '0.00',
16
+ 3 => '#,##0',
17
+ 4 => '#,##0.00',
18
+ 9 => '0%',
19
+ 10 => '0.00%',
20
+ 11 => '0.00E+00',
21
+ 12 => '# ?/?',
22
+ 13 => '# ??/??',
23
+ 14 => 'mm-dd-yy',
24
+ 15 => 'd-mmm-yy',
25
+ 16 => 'd-mmm',
26
+ 17 => 'mmm-yy',
27
+ 18 => 'h:mm AM/PM',
28
+ 19 => 'h:mm:ss AM/PM',
29
+ 20 => 'h:mm',
30
+ 21 => 'h:mm:ss',
31
+ 22 => 'm/d/yy h:mm',
32
+ 37 => '#,##0 ;(#,##0)',
33
+ 38 => '#,##0 ;[Red](#,##0)',
34
+ 39 => '#,##0.00;(#,##0.00)',
35
+ 40 => '#,##0.00;[Red](#,##0.00)',
36
+ 45 => 'mm:ss',
37
+ 46 => '[h]:mm:ss',
38
+ 47 => 'mmss.0',
39
+ 48 => '##0.0E+0',
40
+ 49 => '@'
38
41
  }
39
42
 
40
43
  def to_type(format)
44
+ @to_type ||= {}
45
+ @to_type[format] ||= _to_type(format)
46
+ end
47
+
48
+ def _to_type(format)
41
49
  format = format.to_s.downcase
42
50
  if (type = EXCEPTIONAL_FORMATS[format])
43
51
  type
44
52
  elsif format.include?('#')
45
53
  :float
46
- elsif !format.match(/d+(?![\]])/).nil? || format.include?('y')
54
+ elsif format.include?('y') || !format.match(/d+(?![\]])/).nil?
47
55
  if format.include?('h') || format.include?('s')
48
56
  :datetime
49
57
  else
@@ -58,7 +66,6 @@ module Roo
58
66
  end
59
67
  end
60
68
 
61
- module_function :to_type
62
69
  end
63
- end
70
+ end
64
71
  end
@@ -0,0 +1,26 @@
1
+ require 'roo/excelx/extractor'
2
+
3
+ module Roo
4
+ class Excelx
5
+ class Images < Excelx::Extractor
6
+
7
+ # Returns: Hash { id1: extracted_file_name1 },
8
+ # Example: { "rId1"=>"roo_media_image1.png",
9
+ # "rId2"=>"roo_media_image2.png",
10
+ # "rId3"=>"roo_media_image3.png" }
11
+ def list
12
+ @images ||= extract_images_names
13
+ end
14
+
15
+ private
16
+
17
+ def extract_images_names
18
+ return {} unless doc_exists?
19
+
20
+ doc.xpath('/Relationships/Relationship').each_with_object({}) do |rel, hash|
21
+ hash[rel['Id']] = "roo" + rel['Target'].gsub(/\.\.\/|\//, '_')
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'roo/excelx/extractor'
2
4
 
3
5
  module Roo
@@ -11,14 +13,20 @@ module Roo
11
13
  @relationships ||= extract_relationships
12
14
  end
13
15
 
16
+ def include_type?(type)
17
+ to_a.any? do |_, rel|
18
+ rel["Type"]&.include? type
19
+ end
20
+ end
21
+
14
22
  private
15
23
 
16
24
  def extract_relationships
17
- return [] unless doc_exists?
25
+ return {} unless doc_exists?
18
26
 
19
- Hash[doc.xpath('/Relationships/Relationship').map do |rel|
20
- [rel.attribute('Id').text, rel]
21
- end]
27
+ doc.xpath('/Relationships/Relationship').each_with_object({}) do |rel, hash|
28
+ hash[rel['Id']] = rel
29
+ end
22
30
  end
23
31
  end
24
32
  end
@@ -4,12 +4,15 @@ module Roo
4
4
  # reduce memory usage and reduce the number of objects being passed
5
5
  # to various inititializers.
6
6
  class Shared
7
- attr_accessor :comments_files, :sheet_files, :rels_files
8
- def initialize(dir)
7
+ attr_accessor :comments_files, :sheet_files, :rels_files, :image_rels, :image_files
8
+ def initialize(dir, options = {})
9
9
  @dir = dir
10
10
  @comments_files = []
11
11
  @sheet_files = []
12
12
  @rels_files = []
13
+ @options = options
14
+ @image_rels = []
15
+ @image_files = []
13
16
  end
14
17
 
15
18
  def styles
@@ -17,7 +20,7 @@ module Roo
17
20
  end
18
21
 
19
22
  def shared_strings
20
- @shared_strings ||= SharedStrings.new(File.join(@dir, 'roo_sharedStrings.xml'))
23
+ @shared_strings ||= SharedStrings.new(File.join(@dir, 'roo_sharedStrings.xml'), @options)
21
24
  end
22
25
 
23
26
  def workbook
@@ -27,6 +30,10 @@ module Roo
27
30
  def base_date
28
31
  workbook.base_date
29
32
  end
33
+
34
+ def base_timestamp
35
+ workbook.base_timestamp
36
+ end
30
37
  end
31
38
  end
32
39
  end
@@ -1,16 +1,10 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'roo/excelx/extractor'
2
4
 
3
5
  module Roo
4
6
  class Excelx
5
7
  class SharedStrings < Excelx::Extractor
6
-
7
- COMMON_STRINGS = {
8
- t: "t",
9
- r: "r",
10
- html_tag_open: "<html>",
11
- html_tag_closed: "</html>"
12
- }
13
-
14
8
  def [](index)
15
9
  to_a[index]
16
10
  end
@@ -26,6 +20,7 @@ module Roo
26
20
  # Use to_html or to_a for html returns
27
21
  # See what is happening with commit???
28
22
  def use_html?(index)
23
+ return false if @options[:disable_html_wrapper]
29
24
  to_html[index][/<([biu]|sup|sub)>/]
30
25
  end
31
26
 
@@ -45,7 +40,7 @@ module Roo
45
40
  document = fix_invalid_shared_strings(doc)
46
41
  # read the shared strings xml document
47
42
  document.xpath('/sst/si').map do |si|
48
- shared_string = ''
43
+ shared_string = +""
49
44
  si.children.each do |elem|
50
45
  case elem.name
51
46
  when 'r'
@@ -65,7 +60,7 @@ module Roo
65
60
  fix_invalid_shared_strings(doc)
66
61
  # read the shared strings xml document
67
62
  doc.xpath('/sst/si').map do |si|
68
- html_string = '<html>'
63
+ html_string = '<html>'.dup
69
64
  si.children.each do |elem|
70
65
  case elem.name
71
66
  when 'r'
@@ -95,7 +90,7 @@ module Roo
95
90
  #
96
91
  # Expected Output ::: "<html><sub|sup><b><i><u>TEXT</u></i></b></sub|/sup></html>"
97
92
  def extract_html_r(r_elem)
98
- str = ''
93
+ str = +""
99
94
  xml_elems = {
100
95
  sub: false,
101
96
  sup: false,
@@ -103,7 +98,6 @@ module Roo
103
98
  i: false,
104
99
  u: false
105
100
  }
106
- b, i, u, sub, sup = false, false, false, false, false
107
101
  r_elem.children.each do |elem|
108
102
  case elem.name
109
103
  when 'rPr'
@@ -141,13 +135,13 @@ module Roo
141
135
 
142
136
  # This will return an html string
143
137
  def create_html(text, formatting)
144
- tmp_str = ''
138
+ tmp_str = +""
145
139
  formatting.each do |elem, val|
146
140
  tmp_str << "<#{elem}>" if val
147
141
  end
148
142
  tmp_str << text
149
- reverse_format = Hash[formatting.to_a.reverse]
150
- reverse_format.each do |elem, val|
143
+
144
+ formatting.reverse_each do |elem, val|
151
145
  tmp_str << "</#{elem}>" if val
152
146
  end
153
147
  tmp_str
@@ -4,11 +4,15 @@ module Roo
4
4
  class Sheet
5
5
  extend Forwardable
6
6
 
7
- delegate [:styles, :workbook, :shared_strings, :rels_files, :sheet_files, :comments_files] => :@shared
7
+ delegate [:styles, :workbook, :shared_strings, :rels_files, :sheet_files, :comments_files, :image_rels] => :@shared
8
+
9
+ attr_reader :images
8
10
 
9
11
  def initialize(name, shared, sheet_index, options = {})
10
12
  @name = name
11
13
  @shared = shared
14
+ @sheet_index = sheet_index
15
+ @images = Images.new(image_rels[sheet_index]).list
12
16
  @rels = Relationships.new(rels_files[sheet_index])
13
17
  @comments = Comments.new(comments_files[sheet_index])
14
18
  @sheet = SheetDoc.new(sheet_files[sheet_index], @rels, shared, options)
@@ -19,7 +23,14 @@ module Roo
19
23
  end
20
24
 
21
25
  def present_cells
22
- @present_cells ||= cells.select { |_, cell| cell && !cell.empty? }
26
+ @present_cells ||= begin
27
+ warn %{
28
+ [DEPRECATION] present_cells is deprecated. Alternate:
29
+ with activesupport => cells[key].presence
30
+ without activesupport => cells[key]&.presence
31
+ }
32
+ cells.select { |_, cell| cell&.presence }
33
+ end
23
34
  end
24
35
 
25
36
  # Yield each row as array of Excelx::Cell objects
@@ -39,33 +50,33 @@ module Roo
39
50
 
40
51
  def row(row_number)
41
52
  first_column.upto(last_column).map do |col|
42
- cells[[row_number, col]]
43
- end.map { |cell| cell && cell.value }
53
+ cells[[row_number, col]]&.value
54
+ end
44
55
  end
45
56
 
46
57
  def column(col_number)
47
58
  first_row.upto(last_row).map do |row|
48
- cells[[row, col_number]]
49
- end.map { |cell| cell && cell.value }
59
+ cells[[row, col_number]]&.value
60
+ end
50
61
  end
51
62
 
52
63
  # returns the number of the first non-empty row
53
64
  def first_row
54
- @first_row ||= present_cells.keys.map { |row, _| row }.min
65
+ @first_row ||= first_last_row_col[:first_row]
55
66
  end
56
67
 
57
68
  def last_row
58
- @last_row ||= present_cells.keys.map { |row, _| row }.max
69
+ @last_row ||= first_last_row_col[:last_row]
59
70
  end
60
71
 
61
72
  # returns the number of the first non-empty column
62
73
  def first_column
63
- @first_column ||= present_cells.keys.map { |_, col| col }.min
74
+ @first_column ||= first_last_row_col[:first_column]
64
75
  end
65
76
 
66
77
  # returns the number of the last non-empty column
67
78
  def last_column
68
- @last_column ||= present_cells.keys.map { |_, col| col }.max
79
+ @last_column ||= first_last_row_col[:last_column]
69
80
  end
70
81
 
71
82
  def excelx_format(key)
@@ -107,6 +118,34 @@ module Roo
107
118
  (cell.coordinate.column - 1 - last_column).times { pad << nil }
108
119
  pad
109
120
  end
121
+
122
+ def first_last_row_col
123
+ @first_last_row_col ||= begin
124
+ first_row = last_row = first_col = last_col = nil
125
+
126
+ cells.each do |(row, col), cell|
127
+ next unless cell&.presence
128
+ first_row ||= row
129
+ last_row ||= row
130
+ first_col ||= col
131
+ last_col ||= col
132
+
133
+ if row > last_row
134
+ last_row = row
135
+ elsif row < first_row
136
+ first_row = row
137
+ end
138
+
139
+ if col > last_col
140
+ last_col = col
141
+ elsif col < first_col
142
+ first_col = col
143
+ end
144
+ end
145
+
146
+ {first_row: first_row, last_row: last_row, first_column: first_col, last_column: last_col}
147
+ end
148
+ end
110
149
  end
111
150
  end
112
151
  end