roo 2.3.0 → 2.10.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.
Files changed (95) 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/.github/workflows/pull-request.yml +15 -0
  6. data/.github/workflows/ruby.yml +34 -0
  7. data/.gitignore +4 -0
  8. data/.rubocop.yml +186 -0
  9. data/CHANGELOG.md +148 -0
  10. data/Gemfile +4 -4
  11. data/LICENSE +2 -0
  12. data/README.md +84 -27
  13. data/Rakefile +1 -1
  14. data/lib/roo/base.rb +111 -237
  15. data/lib/roo/constants.rb +5 -3
  16. data/lib/roo/csv.rb +106 -85
  17. data/lib/roo/errors.rb +2 -0
  18. data/lib/roo/excelx/cell/base.rb +26 -12
  19. data/lib/roo/excelx/cell/boolean.rb +9 -6
  20. data/lib/roo/excelx/cell/date.rb +7 -7
  21. data/lib/roo/excelx/cell/datetime.rb +50 -44
  22. data/lib/roo/excelx/cell/empty.rb +3 -2
  23. data/lib/roo/excelx/cell/number.rb +60 -47
  24. data/lib/roo/excelx/cell/string.rb +3 -3
  25. data/lib/roo/excelx/cell/time.rb +17 -16
  26. data/lib/roo/excelx/cell.rb +11 -7
  27. data/lib/roo/excelx/comments.rb +3 -3
  28. data/lib/roo/excelx/coordinate.rb +11 -4
  29. data/lib/roo/excelx/extractor.rb +20 -3
  30. data/lib/roo/excelx/format.rb +38 -31
  31. data/lib/roo/excelx/images.rb +26 -0
  32. data/lib/roo/excelx/relationships.rb +12 -4
  33. data/lib/roo/excelx/shared.rb +10 -3
  34. data/lib/roo/excelx/shared_strings.rb +113 -9
  35. data/lib/roo/excelx/sheet.rb +49 -10
  36. data/lib/roo/excelx/sheet_doc.rb +101 -48
  37. data/lib/roo/excelx/styles.rb +4 -4
  38. data/lib/roo/excelx/workbook.rb +8 -3
  39. data/lib/roo/excelx.rb +85 -42
  40. data/lib/roo/formatters/base.rb +15 -0
  41. data/lib/roo/formatters/csv.rb +84 -0
  42. data/lib/roo/formatters/matrix.rb +23 -0
  43. data/lib/roo/formatters/xml.rb +31 -0
  44. data/lib/roo/formatters/yaml.rb +40 -0
  45. data/lib/roo/helpers/default_attr_reader.rb +20 -0
  46. data/lib/roo/helpers/weak_instance_cache.rb +41 -0
  47. data/lib/roo/open_office.rb +41 -27
  48. data/lib/roo/spreadsheet.rb +8 -2
  49. data/lib/roo/tempdir.rb +24 -0
  50. data/lib/roo/utils.rb +76 -26
  51. data/lib/roo/version.rb +1 -1
  52. data/lib/roo.rb +5 -0
  53. data/roo.gemspec +22 -12
  54. data/spec/lib/roo/base_spec.rb +65 -3
  55. data/spec/lib/roo/csv_spec.rb +19 -0
  56. data/spec/lib/roo/excelx/cell/time_spec.rb +15 -0
  57. data/spec/lib/roo/excelx/relationships_spec.rb +43 -0
  58. data/spec/lib/roo/excelx/sheet_doc_spec.rb +11 -0
  59. data/spec/lib/roo/excelx_spec.rb +237 -5
  60. data/spec/lib/roo/openoffice_spec.rb +2 -2
  61. data/spec/lib/roo/spreadsheet_spec.rb +1 -1
  62. data/spec/lib/roo/strict_spec.rb +43 -0
  63. data/spec/lib/roo/utils_spec.rb +22 -9
  64. data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
  65. data/spec/lib/roo_spec.rb +0 -0
  66. data/spec/spec_helper.rb +2 -7
  67. data/test/excelx/cell/test_attr_reader_default.rb +72 -0
  68. data/test/excelx/cell/test_base.rb +6 -2
  69. data/test/excelx/cell/test_boolean.rb +1 -3
  70. data/test/excelx/cell/test_date.rb +1 -6
  71. data/test/excelx/cell/test_datetime.rb +7 -10
  72. data/test/excelx/cell/test_empty.rb +12 -2
  73. data/test/excelx/cell/test_number.rb +28 -4
  74. data/test/excelx/cell/test_string.rb +21 -3
  75. data/test/excelx/cell/test_time.rb +7 -10
  76. data/test/excelx/test_coordinate.rb +51 -0
  77. data/test/formatters/test_csv.rb +136 -0
  78. data/test/formatters/test_matrix.rb +76 -0
  79. data/test/formatters/test_xml.rb +78 -0
  80. data/test/formatters/test_yaml.rb +20 -0
  81. data/test/helpers/test_accessing_files.rb +81 -0
  82. data/test/helpers/test_comments.rb +43 -0
  83. data/test/helpers/test_formulas.rb +9 -0
  84. data/test/helpers/test_labels.rb +103 -0
  85. data/test/helpers/test_sheets.rb +55 -0
  86. data/test/helpers/test_styles.rb +62 -0
  87. data/test/roo/test_base.rb +182 -0
  88. data/test/roo/test_csv.rb +88 -0
  89. data/test/roo/test_excelx.rb +360 -0
  90. data/test/roo/test_libre_office.rb +9 -0
  91. data/test/roo/test_open_office.rb +289 -0
  92. data/test/test_helper.rb +129 -14
  93. data/test/test_roo.rb +60 -1765
  94. metadata +91 -21
  95. data/.travis.yml +0 -14
@@ -0,0 +1,84 @@
1
+ module Roo
2
+ module Formatters
3
+ module CSV
4
+ def to_csv(filename = nil, separator = ",", sheet = default_sheet)
5
+ if filename
6
+ File.open(filename, "w") do |file|
7
+ write_csv_content(file, sheet, separator)
8
+ end
9
+ true
10
+ else
11
+ sio = ::StringIO.new
12
+ write_csv_content(sio, sheet, separator)
13
+ sio.rewind
14
+ sio.read
15
+ end
16
+ end
17
+
18
+ private
19
+
20
+ # Write all cells to the csv file. File can be a filename or nil. If the
21
+ # file argument is nil the output goes to STDOUT
22
+ def write_csv_content(file = nil, sheet = nil, separator = ",")
23
+ file ||= STDOUT
24
+ return unless first_row(sheet) # The sheet is empty
25
+
26
+ 1.upto(last_row(sheet)) do |row|
27
+ 1.upto(last_column(sheet)) do |col|
28
+ # TODO: use CSV.generate_line
29
+ file.print(separator) if col > 1
30
+ file.print cell_to_csv(row, col, sheet)
31
+ end
32
+ file.print("\n")
33
+ end
34
+ end
35
+
36
+ # The content of a cell in the csv output
37
+ def cell_to_csv(row, col, sheet)
38
+ return "" if empty?(row, col, sheet)
39
+
40
+ onecell = cell(row, col, sheet)
41
+
42
+ case celltype(row, col, sheet)
43
+ when :string
44
+ %("#{onecell.gsub('"', '""')}") unless onecell.empty?
45
+ when :boolean
46
+ # TODO: this only works for excelx
47
+ onecell = self.sheet_for(sheet).cells[[row, col]].formatted_value
48
+ %("#{onecell.gsub('"', '""').downcase}")
49
+ when :float, :percentage
50
+ if onecell == onecell.to_i
51
+ onecell.to_i.to_s
52
+ else
53
+ onecell.to_s
54
+ end
55
+ when :formula
56
+ case onecell
57
+ when String
58
+ %("#{onecell.gsub('"', '""')}") unless onecell.empty?
59
+ when Integer
60
+ onecell.to_s
61
+ when Float
62
+ if onecell == onecell.to_i
63
+ onecell.to_i.to_s
64
+ else
65
+ onecell.to_s
66
+ end
67
+ when Date, DateTime, TrueClass, FalseClass
68
+ onecell.to_s
69
+ else
70
+ fail "unhandled onecell-class #{onecell.class}"
71
+ end
72
+ when :date, :datetime
73
+ onecell.to_s
74
+ when :time
75
+ integer_to_timestring(onecell)
76
+ when :link
77
+ %("#{onecell.url.gsub('"', '""')}")
78
+ else
79
+ fail "unhandled celltype #{celltype(row, col, sheet)}"
80
+ end || ""
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,23 @@
1
+ module Roo
2
+ module Formatters
3
+ module Matrix
4
+ # returns a matrix object from the whole sheet or a rectangular area of a sheet
5
+ def to_matrix(from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet)
6
+ require 'matrix'
7
+
8
+ return ::Matrix.empty unless first_row
9
+
10
+ from_row ||= first_row(sheet)
11
+ to_row ||= last_row(sheet)
12
+ from_column ||= first_column(sheet)
13
+ to_column ||= last_column(sheet)
14
+
15
+ ::Matrix.rows(from_row.upto(to_row).map do |row|
16
+ from_column.upto(to_column).map do |col|
17
+ cell(row, col, sheet)
18
+ end
19
+ end)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,31 @@
1
+ # returns an XML representation of all sheets of a spreadsheet file
2
+ module Roo
3
+ module Formatters
4
+ module XML
5
+ def to_xml
6
+ Nokogiri::XML::Builder.new do |xml|
7
+ xml.spreadsheet do
8
+ sheets.each do |sheet|
9
+ self.default_sheet = sheet
10
+ xml.sheet(name: sheet) do |x|
11
+ if first_row && last_row && first_column && last_column
12
+ # sonst gibt es Fehler bei leeren Blaettern
13
+ first_row.upto(last_row) do |row|
14
+ first_column.upto(last_column) do |col|
15
+ next if empty?(row, col)
16
+
17
+ x.cell(cell(row, col),
18
+ row: row,
19
+ column: col,
20
+ type: celltype(row, col))
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end.to_xml
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,40 @@
1
+ module Roo
2
+ module Formatters
3
+ module YAML
4
+ # returns a rectangular area (default: all cells) as yaml-output
5
+ # you can add additional attributes with the prefix parameter like:
6
+ # oo.to_yaml({"file"=>"flightdata_2007-06-26", "sheet" => "1"})
7
+ def to_yaml(prefix = {}, from_row = nil, from_column = nil, to_row = nil, to_column = nil, sheet = default_sheet)
8
+ # return an empty string if there is no first_row, i.e. the sheet is empty
9
+ return "" unless first_row
10
+
11
+ from_row ||= first_row(sheet)
12
+ to_row ||= last_row(sheet)
13
+ from_column ||= first_column(sheet)
14
+ to_column ||= last_column(sheet)
15
+
16
+ result = "--- \n"
17
+ from_row.upto(to_row) do |row|
18
+ from_column.upto(to_column) do |col|
19
+ next if empty?(row, col, sheet)
20
+
21
+ result << "cell_#{row}_#{col}: \n"
22
+ prefix.each do|k, v|
23
+ result << " #{k}: #{v} \n"
24
+ end
25
+ result << " row: #{row} \n"
26
+ result << " col: #{col} \n"
27
+ result << " celltype: #{celltype(row, col, sheet)} \n"
28
+ value = cell(row, col, sheet)
29
+ if celltype(row, col, sheet) == :time
30
+ value = integer_to_timestring(value)
31
+ end
32
+ result << " value: #{value} \n"
33
+ end
34
+ end
35
+
36
+ result
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ module Helpers
5
+ module DefaultAttrReader
6
+ def attr_reader_with_default(attr_hash)
7
+ attr_hash.each do |attr_name, default_value|
8
+ instance_variable = :"@#{attr_name}"
9
+ define_method attr_name do
10
+ if instance_variable_defined? instance_variable
11
+ instance_variable_get instance_variable
12
+ else
13
+ default_value
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "weakref"
4
+
5
+ module Roo
6
+ module Helpers
7
+ module WeakInstanceCache
8
+ private
9
+
10
+ def instance_cache(key)
11
+ object = nil
12
+
13
+ if instance_variable_defined?(key) && (ref = instance_variable_get(key)) && ref.weakref_alive?
14
+ begin
15
+ object = ref.__getobj__
16
+ rescue => e
17
+ unless (defined?(::WeakRef::RefError) && e.is_a?(::WeakRef::RefError)) || (defined?(RefError) && e.is_a?(RefError))
18
+ raise e
19
+ end
20
+ end
21
+ end
22
+
23
+ unless object
24
+ object = yield
25
+ ObjectSpace.define_finalizer(object, instance_cache_finalizer(key))
26
+ instance_variable_set(key, WeakRef.new(object))
27
+ end
28
+
29
+ object
30
+ end
31
+
32
+ def instance_cache_finalizer(key)
33
+ proc do |object_id|
34
+ if instance_variable_defined?(key) && (ref = instance_variable_get(key)) && (!ref.weakref_alive? || ref.__getobj__.object_id == object_id)
35
+ remove_instance_variable(key)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -1,15 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
  require 'nokogiri'
3
5
  require 'cgi'
4
6
  require 'zip/filesystem'
5
7
  require 'roo/font'
8
+ require 'roo/tempdir'
6
9
  require 'base64'
10
+ require 'openssl'
7
11
 
8
12
  module Roo
9
13
  class OpenOffice < Roo::Base
10
- ERROR_MISSING_CONTENT_XML = 'file missing required content.xml'.freeze
11
- XPATH_FIND_TABLE_STYLES = "//*[local-name()='automatic-styles']".freeze
12
- XPATH_LOCAL_NAME_TABLE = "//*[local-name()='table']".freeze
14
+ extend Roo::Tempdir
15
+
16
+ ERROR_MISSING_CONTENT_XML = 'file missing required content.xml'
17
+ XPATH_FIND_TABLE_STYLES = "//*[local-name()='automatic-styles']"
18
+ XPATH_LOCAL_NAME_TABLE = "//*[local-name()='table']"
13
19
 
14
20
  # initialization and opening of a spreadsheet file
15
21
  # values for packed: :zip
@@ -19,15 +25,32 @@ module Roo
19
25
 
20
26
  @only_visible_sheets = options[:only_visible_sheets]
21
27
  file_type_check(filename, '.ods', 'an Roo::OpenOffice', file_warning, packed)
22
- @tmpdir = make_tmpdir(File.basename(filename), options[:tmpdir_root])
28
+ # NOTE: Create temp directory and allow Ruby to cleanup the temp directory
29
+ # when the object is garbage collected. Initially, the finalizer was
30
+ # created in the Roo::Tempdir module, but that led to a segfault
31
+ # when testing in Ruby 2.4.0.
32
+ @tmpdir = self.class.make_tempdir(self, find_basename(filename), options[:tmpdir_root])
33
+ ObjectSpace.define_finalizer(self, self.class.finalize(object_id))
23
34
  @filename = local_filename(filename, @tmpdir, packed)
24
35
  # TODO: @cells_read[:default] = false
25
36
  open_oo_file(options)
26
37
  super(filename, options)
27
38
  initialize_default_variables
28
- rescue => e # clean up any temp files, but only if an error was raised
29
- close
30
- raise e
39
+
40
+ unless @table_display.any?
41
+ doc.xpath(XPATH_FIND_TABLE_STYLES).each do |style|
42
+ read_table_styles(style)
43
+ end
44
+ end
45
+
46
+ @sheet_names = doc.xpath(XPATH_LOCAL_NAME_TABLE).map do |sheet|
47
+ if !@only_visible_sheets || @table_display[attribute(sheet, 'style-name')]
48
+ sheet.attributes['name'].value
49
+ end
50
+ end.compact
51
+ rescue
52
+ self.class.finalize_tempdirs(object_id)
53
+ raise
31
54
  end
32
55
 
33
56
  def open_oo_file(options)
@@ -132,16 +155,7 @@ module Roo
132
155
  end
133
156
 
134
157
  def sheets
135
- unless @table_display.any?
136
- doc.xpath(XPATH_FIND_TABLE_STYLES).each do |style|
137
- read_table_styles(style)
138
- end
139
- end
140
- doc.xpath(XPATH_LOCAL_NAME_TABLE).map do |sheet|
141
- if !@only_visible_sheets || @table_display[attribute(sheet, 'style-name')]
142
- sheet.attributes['name'].value
143
- end
144
- end.compact
158
+ @sheet_names
145
159
  end
146
160
 
147
161
  # version of the Roo::OpenOffice document
@@ -285,7 +299,6 @@ module Roo
285
299
  algorithm_node['manifest:initialisation-vector']
286
300
  )
287
301
  key_derivation_name = key_derivation_node['manifest:key-derivation-name']
288
- key_size = key_derivation_node['manifest:key-size'].to_i
289
302
  iteration_count = key_derivation_node['manifest:iteration-count'].to_i
290
303
  salt = Base64.decode64(key_derivation_node['manifest:salt'])
291
304
 
@@ -294,10 +307,8 @@ module Roo
294
307
  start_key_generation_node[
295
308
  'manifest:start-key-generation-name'
296
309
  ]
297
- key_generation_size = start_key_generation_node['manifest:key-size'].to_i
298
310
 
299
311
  hashed_password = password
300
- key = nil
301
312
 
302
313
  if key_generation_name == 'http://www.w3.org/2000/09/xmldsig#sha256'
303
314
 
@@ -337,7 +348,7 @@ module Roo
337
348
  def find_cipher(*args)
338
349
  fail ArgumentError, 'Unknown algorithm ' + algorithm unless args[0] == 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
339
350
 
340
- cipher = OpenSSL::Cipher.new('AES-256-CBC')
351
+ cipher = ::OpenSSL::Cipher.new('AES-256-CBC')
341
352
  cipher.decrypt
342
353
  cipher.padding = 0
343
354
  cipher.key = find_cipher_key(cipher, *args[1..4])
@@ -350,7 +361,7 @@ module Roo
350
361
  def find_cipher_key(*args)
351
362
  fail ArgumentError, 'Unknown key derivation name ', args[1] unless args[1] == 'PBKDF2'
352
363
 
353
- OpenSSL::PKCS5.pbkdf2_hmac_sha1(args[2], args[3], args[4], args[0].key_len)
364
+ ::OpenSSL::PKCS5.pbkdf2_hmac_sha1(args[2], args[3], args[4], args[0].key_len)
354
365
  end
355
366
 
356
367
  # Block decrypt raw bytes from the zip file based on the cipher
@@ -412,7 +423,10 @@ module Roo
412
423
  @style[sheet][key] = style_name
413
424
  case @cell_type[sheet][key]
414
425
  when :float
415
- @cell[sheet][key] = (table_cell.attributes['value'].to_s.include?(".") || table_cell.children.first.text.include?(".")) ? v.to_f : v.to_i
426
+ value = (table_cell.attributes['value'].to_s.include?(".") || table_cell.children.first.text.include?(".")) ? v.to_f : v.to_i
427
+ value = 'true' if formula == '=TRUE()'
428
+ value = 'false' if formula == '=FALSE()'
429
+ @cell[sheet][key] = value
416
430
  when :percentage
417
431
  @cell[sheet][key] = v.to_f
418
432
  when :string
@@ -506,7 +520,7 @@ module Roo
506
520
  str_v += child.content #.text
507
521
  end
508
522
  end
509
- str_v.gsub!(/&apos;/, "'") # special case not supported by unescapeHTML
523
+ str_v = str_v.gsub(/&apos;/, "'") # special case not supported by unescapeHTML
510
524
  str_v = CGI.unescapeHTML(str_v)
511
525
  end # == 'p'
512
526
  end
@@ -552,7 +566,7 @@ module Roo
552
566
  end
553
567
 
554
568
  def read_labels
555
- @label ||= Hash[doc.xpath('//table:named-range').map do |ne|
569
+ @label ||= doc.xpath('//table:named-range').each_with_object({}) do |ne, hash|
556
570
  #-
557
571
  # $Sheet1.$C$5
558
572
  #+
@@ -560,8 +574,8 @@ module Roo
560
574
  sheetname, coords = attribute(ne, 'cell-range-address').to_s.split('.$')
561
575
  col, row = coords.split('$')
562
576
  sheetname = sheetname[1..-1] if sheetname[0, 1] == '$'
563
- [name, [sheetname, row, col]]
564
- end]
577
+ hash[name] = [sheetname, row, col]
578
+ end
565
579
  end
566
580
 
567
581
  def read_styles(style_elements)
@@ -24,8 +24,14 @@ module Roo
24
24
  options[:file_warning] = :ignore
25
25
  extension.tr('.', '').downcase.to_sym
26
26
  else
27
- res = ::File.extname((path =~ /\A#{::URI.regexp}\z/) ? ::URI.parse(::URI.encode(path)).path : path)
28
- res.tr('.', '').downcase.to_sym
27
+ parsed_path =
28
+ if path =~ /\A#{::URI::DEFAULT_PARSER.make_regexp}\z/
29
+ # path is 7th match
30
+ Regexp.last_match[7]
31
+ else
32
+ path
33
+ end
34
+ ::File.extname(parsed_path).tr('.', '').downcase.to_sym
29
35
  end
30
36
  end
31
37
  end
@@ -0,0 +1,24 @@
1
+ module Roo
2
+ module Tempdir
3
+ def finalize_tempdirs(object_id)
4
+ if @tempdirs && (dirs_to_remove = @tempdirs[object_id])
5
+ @tempdirs.delete(object_id)
6
+ dirs_to_remove.each do |dir|
7
+ # Pass force=true to avoid an exception (and thus warnings in Ruby 3.1) if dir has
8
+ # already been removed. This can occur when the finalizer is called both in a forked
9
+ # child process and in the parent.
10
+ ::FileUtils.remove_entry(dir, true)
11
+ end
12
+ end
13
+ end
14
+
15
+ def make_tempdir(object, prefix, root)
16
+ root ||= ENV["ROO_TMP"]
17
+ # NOTE: This folder is cleaned up by finalize_tempdirs.
18
+ ::Dir.mktmpdir("#{Roo::TEMP_PREFIX}#{prefix}", root).tap do |tmpdir|
19
+ @tempdirs ||= Hash.new { |h, k| h[k] = [] }
20
+ @tempdirs[object.object_id] << tmpdir
21
+ end
22
+ end
23
+ end
24
+ end
data/lib/roo/utils.rb CHANGED
@@ -1,42 +1,56 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Roo
2
4
  module Utils
3
5
  extend self
4
6
 
5
- def split_coordinate(str)
6
- @split_coordinate ||= {}
7
+ LETTERS = ('A'..'Z').to_a
8
+
9
+ def extract_coordinate(s)
10
+ num = letter_num = 0
11
+ num_only = false
7
12
 
8
- @split_coordinate[str] ||= begin
9
- letter, number = split_coord(str)
10
- x = letter_to_number(letter)
11
- y = number
12
- [y, x]
13
+ s.each_byte do |b|
14
+ if !num_only && (index = char_index(b))
15
+ letter_num *= 26
16
+ letter_num += index
17
+ elsif index = num_index(b)
18
+ num_only = true
19
+ num *= 10
20
+ num += index
21
+ else
22
+ fail ArgumentError
23
+ end
13
24
  end
25
+ fail ArgumentError if letter_num == 0 || !num_only
26
+
27
+ Excelx::Coordinate.new(num, letter_num)
14
28
  end
15
29
 
16
- alias_method :ref_to_key, :split_coordinate
30
+ alias_method :ref_to_key, :extract_coordinate
17
31
 
18
- def split_coord(s)
19
- if s =~ /([a-zA-Z]+)([0-9]+)/
20
- letter = Regexp.last_match[1]
21
- number = Regexp.last_match[2].to_i
22
- else
23
- fail ArgumentError
24
- end
25
- [letter, number]
32
+ def split_coordinate(str)
33
+ warn "[DEPRECATION] `Roo::Utils.split_coordinate` is deprecated. Please use `Roo::Utils.extract_coordinate` instead."
34
+ extract_coordinate(str)
35
+ end
36
+
37
+
38
+
39
+ def split_coord(str)
40
+ coord = extract_coordinate(str)
41
+ [number_to_letter(coord.column), coord.row]
26
42
  end
27
43
 
28
44
  # convert a number to something like 'AB' (1 => 'A', 2 => 'B', ...)
29
45
  def number_to_letter(num)
30
- results = []
31
- num = num.to_i
46
+ result = +""
32
47
 
33
- while (num > 0)
34
- mod = (num - 1) % 26
35
- results = [(65 + mod).chr] + results
36
- num = ((num - mod) / 26)
48
+ until num.zero?
49
+ num, index = (num - 1).divmod(26)
50
+ result.prepend(LETTERS[index])
37
51
  end
38
52
 
39
- results.join
53
+ result
40
54
  end
41
55
 
42
56
  def letter_to_number(letters)
@@ -56,11 +70,30 @@ module Roo
56
70
  cells = str.split(':')
57
71
  return 1 if cells.count == 1
58
72
  raise ArgumentError.new("invalid range string: #{str}. Supported range format 'A1:B2'") if cells.count != 2
59
- x1, y1 = split_coordinate(cells[0])
60
- x2, y2 = split_coordinate(cells[1])
73
+ x1, y1 = extract_coordinate(cells[0])
74
+ x2, y2 = extract_coordinate(cells[1])
61
75
  (x2 - (x1 - 1)) * (y2 - (y1 - 1))
62
76
  end
63
77
 
78
+ def coordinates_in_range(str)
79
+ return to_enum(:coordinates_in_range, str) unless block_given?
80
+ coordinates = str.split(":", 2).map! { |s| extract_coordinate s }
81
+
82
+ case coordinates.size
83
+ when 1
84
+ yield coordinates[0]
85
+ when 2
86
+ tl, br = coordinates
87
+ rows = tl.row..br.row
88
+ cols = tl.column..br.column
89
+ rows.each do |row|
90
+ cols.each do |column|
91
+ yield Excelx::Coordinate.new(row, column)
92
+ end
93
+ end
94
+ end
95
+ end
96
+
64
97
  def load_xml(path)
65
98
  ::File.open(path, 'rb') do |file|
66
99
  ::Nokogiri::XML(file)
@@ -69,10 +102,27 @@ module Roo
69
102
 
70
103
  # Yield each element of a given type ('row', 'c', etc.) to caller
71
104
  def each_element(path, elements)
105
+ elements = Array(elements)
72
106
  Nokogiri::XML::Reader(::File.open(path, 'rb'), nil, nil, Nokogiri::XML::ParseOptions::NOBLANKS).each do |node|
73
- next unless node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT && Array(elements).include?(node.name)
107
+ next unless node.node_type == Nokogiri::XML::Reader::TYPE_ELEMENT && elements.include?(node.name)
74
108
  yield Nokogiri::XML(node.outer_xml).root if block_given?
75
109
  end
76
110
  end
111
+
112
+ private
113
+
114
+ def char_index(byte)
115
+ if byte >= 65 && byte <= 90
116
+ byte - 64
117
+ elsif byte >= 97 && byte <= 122
118
+ byte - 96
119
+ end
120
+ end
121
+
122
+ def num_index(byte)
123
+ if byte >= 48 && byte <= 57
124
+ byte - 48
125
+ end
126
+ end
77
127
  end
78
128
  end
data/lib/roo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Roo
2
- VERSION = '2.3.0'
2
+ VERSION = "2.10.1"
3
3
  end
data/lib/roo.rb CHANGED
@@ -1,3 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'roo/version'
1
4
  require 'roo/constants'
2
5
  require 'roo/errors'
3
6
  require 'roo/spreadsheet'
@@ -9,6 +12,8 @@ module Roo
9
12
  autoload :Excelx, 'roo/excelx'
10
13
  autoload :CSV, 'roo/csv'
11
14
 
15
+ TEMP_PREFIX = 'roo_'
16
+
12
17
  CLASS_FOR_EXTENSION = {
13
18
  ods: Roo::OpenOffice,
14
19
  xlsx: Roo::Excelx,
data/roo.gemspec CHANGED
@@ -4,22 +4,32 @@ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
4
  require 'roo/version'
5
5
 
6
6
  Gem::Specification.new do |spec|
7
- spec.name = 'roo'
8
- spec.version = Roo::VERSION
9
- spec.authors = ['Thomas Preymesser', 'Hugh McGowan', 'Ben Woosley', 'Oleksandr Simonov', 'Steven Daniels']
10
- spec.email = ['ruby.ruby.ruby.roo@gmail.com', 'oleksandr@simonov.me']
11
- spec.summary = 'Roo can access the contents of various spreadsheet files.'
12
- spec.description = "Roo can access the contents of various spreadsheet files. It can handle\n* OpenOffice\n* Excelx\n* LibreOffice\n* CSV"
13
- spec.homepage = 'http://github.com/roo-rb/roo'
14
- spec.license = 'MIT'
7
+ spec.name = 'roo'
8
+ spec.version = Roo::VERSION
9
+ spec.authors = ['Thomas Preymesser', 'Hugh McGowan', 'Ben Woosley', 'Oleksandr Simonov', 'Steven Daniels', 'Anmol Chopra']
10
+ spec.email = ['ruby.ruby.ruby.roo@gmail.com', 'oleksandr@simonov.me']
11
+ spec.summary = 'Roo can access the contents of various spreadsheet files.'
12
+ spec.description = "Roo can access the contents of various spreadsheet files. It can handle\n* OpenOffice\n* Excelx\n* LibreOffice\n* CSV"
13
+ spec.homepage = 'https://github.com/roo-rb/roo'
14
+ spec.license = 'MIT'
15
15
 
16
- spec.files = `git ls-files -z`.split("\x0")
16
+ spec.files = `git ls-files -z`.split("\x0")
17
17
  spec.files.reject! { |fn| fn.include?('test/files') }
18
- spec.require_paths = ['lib']
18
+ spec.require_paths = ['lib']
19
+
20
+ if defined?(RUBY_ENGINE) && RUBY_ENGINE == 'jruby'
21
+ spec.required_ruby_version = ">= 2.6.0"
22
+ else
23
+ spec.required_ruby_version = ">= 2.7.0"
24
+ end
19
25
 
20
26
  spec.add_dependency 'nokogiri', '~> 1'
21
- spec.add_dependency 'rubyzip', '~> 1.1', '< 2.0.0'
27
+ spec.add_dependency 'rubyzip', '>= 1.3.0', '< 3.0.0'
22
28
 
23
- spec.add_development_dependency 'rake', '~> 10.1'
29
+ spec.add_development_dependency 'rake'
24
30
  spec.add_development_dependency 'minitest', '~> 5.4', '>= 5.4.3'
31
+ spec.add_development_dependency 'rack', '~> 1.6', '< 2.0.0'
32
+ if RUBY_VERSION >= '3.0.0'
33
+ spec.add_development_dependency 'matrix'
34
+ end
25
35
  end