roo 2.6.0 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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