roo 2.6.0 → 2.7.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.
@@ -4,12 +4,11 @@ require 'roo/link'
4
4
  require 'roo/tempdir'
5
5
  require 'roo/utils'
6
6
  require 'forwardable'
7
+ require 'set'
7
8
 
8
9
  module Roo
9
10
  class Excelx < Roo::Base
10
11
  extend Roo::Tempdir
11
-
12
- require 'set'
13
12
  extend Forwardable
14
13
 
15
14
  ERROR_VALUES = %w(#N/A #REF! #NAME? #DIV/0! #NULL! #VALUE! #NUM!).to_set
@@ -46,7 +45,13 @@ module Roo
46
45
  basename = find_basename(filename_or_stream)
47
46
  end
48
47
 
48
+ # NOTE: Create temp directory and allow Ruby to cleanup the temp directory
49
+ # when the object is garbage collected. Initially, the finalizer was
50
+ # created in the Roo::Tempdir module, but that led to a segfault
51
+ # when testing in Ruby 2.4.0.
49
52
  @tmpdir = self.class.make_tempdir(self, basename, options[:tmpdir_root])
53
+ ObjectSpace.define_finalizer(self, self.class.finalize(object_id))
54
+
50
55
  @shared = Shared.new(@tmpdir)
51
56
  @filename = local_filename(filename_or_stream, @tmpdir, packed)
52
57
  process_zipfile(@filename || filename_or_stream)
@@ -218,7 +223,7 @@ module Roo
218
223
  sheet = sheet_for(sheet)
219
224
  key = normalize(row, col)
220
225
  cell = sheet.cells[key]
221
- !cell || cell.empty? || (cell.type == :string && cell.value.empty?) ||
226
+ !cell || cell.empty? ||
222
227
  (row < sheet.first_row || row > sheet.last_row || col < sheet.first_column || col > sheet.last_column)
223
228
  end
224
229
 
@@ -32,14 +32,9 @@ module Roo
32
32
  #
33
33
  # Returns a String representation of a cell's value.
34
34
  def formatted_value
35
- date_regex = /(?<date>[dmy]+[\-\/][dmy]+([\-\/][dmy]+)?)/
36
- time_regex = /(?<time>(\[?[h]\]?+:)?[m]+(:?ss|:?s)?)/
37
-
38
35
  formatter = @format.downcase.split(' ').map do |part|
39
- if part[date_regex] == part
40
- part.gsub(/#{DATE_FORMATS.keys.join('|')}/, DATE_FORMATS)
41
- elsif part[time_regex]
42
- part.gsub(/#{TIME_FORMATS.keys.join('|')}/, TIME_FORMATS)
36
+ if (parsed_format = parse_date_or_time_format(part))
37
+ parsed_format
43
38
  else
44
39
  warn 'Unable to parse custom format. Using "YYYY-mm-dd HH:MM:SS" format.'
45
40
  return @value.strftime('%F %T')
@@ -51,35 +46,50 @@ module Roo
51
46
 
52
47
  private
53
48
 
49
+ def parse_date_or_time_format(part)
50
+ date_regex = /(?<date>[dmy]+[\-\/][dmy]+([\-\/][dmy]+)?)/
51
+ time_regex = /(?<time>(\[?[h]\]?+:)?[m]+(:?ss|:?s)?)/
52
+
53
+ if part[date_regex] == part
54
+ formats = DATE_FORMATS
55
+ elsif part[time_regex]
56
+ formats = TIME_FORMATS
57
+ else
58
+ return false
59
+ end
60
+
61
+ part.gsub(/#{formats.keys.join('|')}/, formats)
62
+ end
63
+
54
64
  DATE_FORMATS = {
55
- 'yyyy'.freeze => '%Y'.freeze, # Year: 2000
56
- 'yy'.freeze => '%y'.freeze, # Year: 00
65
+ 'yyyy' => '%Y', # Year: 2000
66
+ 'yy' => '%y', # Year: 00
57
67
  # mmmmm => J-D
58
- 'mmmm'.freeze => '%B'.freeze, # Month: January
59
- 'mmm'.freeze => '%^b'.freeze, # Month: JAN
60
- 'mm'.freeze => '%m'.freeze, # Month: 01
61
- 'm'.freeze => '%-m'.freeze, # Month: 1
62
- 'dddd'.freeze => '%A'.freeze, # Day of the Week: Sunday
63
- 'ddd'.freeze => '%^a'.freeze, # Day of the Week: SUN
64
- 'dd'.freeze => '%d'.freeze, # Day of the Month: 01
65
- 'd'.freeze => '%-d'.freeze, # Day of the Month: 1
68
+ 'mmmm' => '%B', # Month: January
69
+ 'mmm' => '%^b', # Month: JAN
70
+ 'mm' => '%m', # Month: 01
71
+ 'm' => '%-m', # Month: 1
72
+ 'dddd' => '%A', # Day of the Week: Sunday
73
+ 'ddd' => '%^a', # Day of the Week: SUN
74
+ 'dd' => '%d', # Day of the Month: 01
75
+ 'd' => '%-d' # Day of the Month: 1
66
76
  # '\\\\'.freeze => ''.freeze, # NOTE: Fixes a custom format's output.
67
77
  }
68
78
 
69
79
  TIME_FORMATS = {
70
- 'hh'.freeze => '%H'.freeze, # Hour (24): 01
71
- 'h'.freeze => '%-k'.freeze, # Hour (24): 1
80
+ 'hh' => '%H', # Hour (24): 01
81
+ 'h' => '%-k'.freeze, # Hour (24): 1
72
82
  # 'hh'.freeze => '%I'.freeze, # Hour (12): 08
73
83
  # 'h'.freeze => '%-l'.freeze, # Hour (12): 8
74
- 'mm'.freeze => '%M'.freeze, # Minute: 01
84
+ 'mm' => '%M', # Minute: 01
75
85
  # FIXME: is this used? Seems like 'm' is used for month, not minute.
76
- 'm'.freeze => '%-M'.freeze, # Minute: 1
77
- 'ss'.freeze => '%S'.freeze, # Seconds: 01
78
- 's'.freeze => '%-S'.freeze, # Seconds: 1
79
- 'am/pm'.freeze => '%p'.freeze, # Meridian: AM
80
- '000'.freeze => '%3N'.freeze, # Fractional Seconds: thousandth.
81
- '00'.freeze => '%2N'.freeze, # Fractional Seconds: hundredth.
82
- '0'.freeze => '%1N'.freeze, # Fractional Seconds: tenths.
86
+ 'm' => '%-M', # Minute: 1
87
+ 'ss' => '%S', # Seconds: 01
88
+ 's' => '%-S', # Seconds: 1
89
+ 'am/pm' => '%p', # Meridian: AM
90
+ '000' => '%3N', # Fractional Seconds: thousandth.
91
+ '00' => '%2N', # Fractional Seconds: hundredth.
92
+ '0' => '%1N' # Fractional Seconds: tenths.
83
93
  }
84
94
 
85
95
  def create_datetime(base_date, value)
@@ -93,7 +103,7 @@ module Roo
93
103
  def round_datetime(datetime_string)
94
104
  /(?<yyyy>\d+)-(?<mm>\d+)-(?<dd>\d+) (?<hh>\d+):(?<mi>\d+):(?<ss>\d+.\d+)/ =~ datetime_string
95
105
 
96
- ::Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0)
106
+ ::Time.new(yyyy, mm, dd, hh, mi, ss.to_r).round(0)
97
107
  end
98
108
  end
99
109
  end
@@ -32,7 +32,7 @@ module Roo
32
32
  if formatter.is_a? Proc
33
33
  formatter.call(@cell_value)
34
34
  elsif zero_padded_number?
35
- "%0#{@format.size}d"% @cell_value
35
+ "%0#{@format.size}d" % @cell_value
36
36
  else
37
37
  Kernel.format(formatter, @cell_value)
38
38
  end
@@ -45,12 +45,9 @@ module Roo
45
45
  'General' => '%.0f',
46
46
  '0' => '%.0f',
47
47
  '0.00' => '%.2f',
48
- '#,##0' => proc do |number|
49
- Kernel.format('%.0f', number).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
50
- end,
51
- '#,##0.00' => proc do |number|
52
- Kernel.format('%.2f', number).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
53
- end,
48
+ '0.000000' => '%.6f',
49
+ '#,##0' => number_format('%.0f'),
50
+ '#,##0.00' => number_format('%.2f'),
54
51
  '0%' => proc do |number|
55
52
  Kernel.format('%d%', number.to_f * 100)
56
53
  end,
@@ -58,22 +55,10 @@ module Roo
58
55
  Kernel.format('%.2f%', number.to_f * 100)
59
56
  end,
60
57
  '0.00E+00' => '%.2E',
61
- '#,##0 ;(#,##0)' => proc do |number|
62
- formatter = number.to_i > 0 ? '%.0f' : '(%.0f)'
63
- Kernel.format(formatter, number.to_f.abs).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
64
- end,
65
- '#,##0 ;[Red](#,##0)' => proc do |number|
66
- formatter = number.to_i > 0 ? '%.0f' : '[Red](%.0f)'
67
- Kernel.format(formatter, number.to_f.abs).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
68
- end,
69
- '#,##0.00;(#,##0.00)' => proc do |number|
70
- formatter = number.to_i > 0 ? '%.2f' : '(%.2f)'
71
- Kernel.format(formatter, number.to_f.abs).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
72
- end,
73
- '#,##0.00;[Red](#,##0.00)' => proc do |number|
74
- formatter = number.to_i > 0 ? '%.2f' : '[Red](%.2f)'
75
- Kernel.format(formatter, number.to_f.abs).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
76
- end,
58
+ '#,##0 ;(#,##0)' => number_format('%.0f', '(%.0f)'),
59
+ '#,##0 ;[Red](#,##0)' => number_format('%.0f', '[Red](%.0f)'),
60
+ '#,##0.00;(#,##0.00)' => number_format('%.2f', '(%.2f)'),
61
+ '#,##0.00;[Red](#,##0.00)' => number_format('%.2f', '[Red](%.2f)'),
77
62
  # FIXME: not quite sure what the format should look like in this case.
78
63
  '##0.0E+0' => '%.1E',
79
64
  '@' => proc { |number| number }
@@ -82,6 +67,17 @@ module Roo
82
67
 
83
68
  private
84
69
 
70
+ def number_format(formatter, negative_formatter = nil)
71
+ proc do |number|
72
+ if negative_formatter
73
+ formatter = number.to_i > 0 ? formatter : negative_formatter
74
+ number = number.to_f.abs
75
+ end
76
+
77
+ Kernel.format(formatter, number).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
78
+ end
79
+ end
80
+
85
81
  def zero_padded_number?
86
82
  @format[/0+/] == @format
87
83
  end
@@ -24,19 +24,19 @@ module Roo
24
24
 
25
25
  private
26
26
 
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
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
40
40
  end
41
41
  end
42
42
  end
@@ -0,0 +1,15 @@
1
+ module Roo
2
+ module Formatters
3
+ module Base
4
+ # converts an integer value to a time string like '02:05:06'
5
+ def integer_to_timestring(content)
6
+ h = (content / 3600.0).floor
7
+ content -= h * 3600
8
+ m = (content / 60.0).floor
9
+ content -= m * 60
10
+ s = content
11
+ Kernel.format("%02d:%02d:%02d", h, m, s)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -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
@@ -5,6 +5,7 @@ require 'zip/filesystem'
5
5
  require 'roo/font'
6
6
  require 'roo/tempdir'
7
7
  require 'base64'
8
+ require 'openssl'
8
9
 
9
10
  module Roo
10
11
  class OpenOffice < Roo::Base
@@ -22,7 +23,12 @@ module Roo
22
23
 
23
24
  @only_visible_sheets = options[:only_visible_sheets]
24
25
  file_type_check(filename, '.ods', 'an Roo::OpenOffice', file_warning, packed)
25
- @tmpdir = self.class.make_tempdir(self, find_basename(filename), options[:tmpdir_root])
26
+ # NOTE: Create temp directory and allow Ruby to cleanup the temp directory
27
+ # when the object is garbage collected. Initially, the finalizer was
28
+ # created in the Roo::Tempdir module, but that led to a segfault
29
+ # when testing in Ruby 2.4.0.
30
+ @tmpdir = self.class.make_tempdir(self, find_basename(filename), options[:tmpdir_root])
31
+ ObjectSpace.define_finalizer(self, self.class.finalize(object_id))
26
32
  @filename = local_filename(filename, @tmpdir, packed)
27
33
  # TODO: @cells_read[:default] = false
28
34
  open_oo_file(options)
@@ -340,7 +346,7 @@ module Roo
340
346
  def find_cipher(*args)
341
347
  fail ArgumentError, 'Unknown algorithm ' + algorithm unless args[0] == 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'
342
348
 
343
- cipher = OpenSSL::Cipher.new('AES-256-CBC')
349
+ cipher = ::OpenSSL::Cipher.new('AES-256-CBC')
344
350
  cipher.decrypt
345
351
  cipher.padding = 0
346
352
  cipher.key = find_cipher_key(cipher, *args[1..4])
@@ -353,7 +359,7 @@ module Roo
353
359
  def find_cipher_key(*args)
354
360
  fail ArgumentError, 'Unknown key derivation name ', args[1] unless args[1] == 'PBKDF2'
355
361
 
356
- OpenSSL::PKCS5.pbkdf2_hmac_sha1(args[2], args[3], args[4], args[0].key_len)
362
+ ::OpenSSL::PKCS5.pbkdf2_hmac_sha1(args[2], args[3], args[4], args[0].key_len)
357
363
  end
358
364
 
359
365
  # Block decrypt raw bytes from the zip file based on the cipher