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
data/lib/roo/csv.rb CHANGED
@@ -1,5 +1,7 @@
1
- require 'csv'
2
- require 'time'
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "time"
3
5
 
4
6
  # The CSV class can read csv files (must be separated with commas) which then
5
7
  # can be handled like spreadsheets. This means you can access cells like A5
@@ -9,124 +11,125 @@ require 'time'
9
11
  #
10
12
  # You can pass options to the underlying CSV parse operation, via the
11
13
  # :csv_options option.
12
- #
14
+ module Roo
15
+ class CSV < Roo::Base
16
+ attr_reader :filename
17
+
18
+ # Returns an array with the names of the sheets. In CSV class there is only
19
+ # one dummy sheet, because a csv file cannot have more than one sheet.
20
+ def sheets
21
+ ["default"]
22
+ end
13
23
 
14
- class Roo::CSV < Roo::Base
24
+ def cell(row, col, sheet = nil)
25
+ sheet ||= default_sheet
26
+ read_cells(sheet)
27
+ @cell[normalize(row, col)]
28
+ end
15
29
 
16
- attr_reader :filename
30
+ def celltype(row, col, sheet = nil)
31
+ sheet ||= default_sheet
32
+ read_cells(sheet)
33
+ @cell_type[normalize(row, col)]
34
+ end
17
35
 
18
- # Returns an array with the names of the sheets. In CSV class there is only
19
- # one dummy sheet, because a csv file cannot have more than one sheet.
20
- def sheets
21
- ['default']
22
- end
36
+ def cell_postprocessing(_row, _col, value)
37
+ value
38
+ end
23
39
 
24
- def cell(row, col, sheet=nil)
25
- sheet ||= default_sheet
26
- read_cells(sheet)
27
- @cell[normalize(row,col)]
28
- end
40
+ def csv_options
41
+ @options[:csv_options] || {}
42
+ end
29
43
 
30
- def celltype(row, col, sheet=nil)
31
- sheet ||= default_sheet
32
- read_cells(sheet)
33
- @cell_type[normalize(row,col)]
34
- end
44
+ def set_value(row, col, value, _sheet)
45
+ @cell[[row, col]] = value
46
+ end
35
47
 
36
- def cell_postprocessing(row,col,value)
37
- value
38
- end
48
+ def set_type(row, col, type, _sheet)
49
+ @cell_type[[row, col]] = type
50
+ end
39
51
 
40
- def csv_options
41
- @options[:csv_options] || {}
42
- end
52
+ private
43
53
 
44
- def set_value(row, col, value, _sheet)
45
- @cell[[row, col]] = value
46
- end
54
+ TYPE_MAP = {
55
+ String => :string,
56
+ Float => :float,
57
+ Date => :date,
58
+ DateTime => :datetime,
59
+ }
47
60
 
48
- def set_type(row, col, type, _sheet)
49
- @cell_type[[row, col]] = type
50
- end
61
+ def celltype_class(value)
62
+ TYPE_MAP[value.class]
63
+ end
51
64
 
52
- private
65
+ def read_cells(sheet = default_sheet)
66
+ sheet ||= default_sheet
67
+ return if @cells_read[sheet]
68
+ row_num = 0
69
+ max_col_num = 0
70
+
71
+ each_row csv_options do |row|
72
+ row_num += 1
73
+ col_num = 0
74
+
75
+ row.each do |elem|
76
+ col_num += 1
77
+ coordinate = [row_num, col_num]
78
+ @cell[coordinate] = elem
79
+ @cell_type[coordinate] = celltype_class(elem)
80
+ end
53
81
 
54
- TYPE_MAP = {
55
- String => :string,
56
- Float => :float,
57
- Date => :date,
58
- DateTime => :datetime,
59
- }
82
+ max_col_num = col_num if col_num > max_col_num
83
+ end
60
84
 
61
- def celltype_class(value)
62
- TYPE_MAP[value.class]
63
- end
85
+ set_row_count(sheet, row_num)
86
+ set_column_count(sheet, max_col_num)
87
+ @cells_read[sheet] = true
88
+ end
64
89
 
65
- def each_row(options, &block)
66
- if uri?(filename)
67
- ::Dir.mktmpdir(Roo::TEMP_PREFIX, ENV['ROO_TMP']) do |tmpdir|
68
- tmp_filename = download_uri(filename, tmpdir)
69
- CSV.foreach(tmp_filename, options, &block)
90
+ def each_row(options, &block)
91
+ if uri?(filename)
92
+ each_row_using_tempdir(options, &block)
93
+ elsif is_stream?(filename_or_stream)
94
+ ::CSV.new(filename_or_stream, options).each(&block)
95
+ else
96
+ ::CSV.foreach(filename, options, &block)
70
97
  end
71
- elsif is_stream?(filename_or_stream)
72
- CSV.new(filename_or_stream, options).each(&block)
73
- else
74
- CSV.foreach(filename, options, &block)
75
98
  end
76
- end
77
99
 
78
- def read_cells(sheet = default_sheet)
79
- sheet ||= default_sheet
80
- return if @cells_read[sheet]
81
- @first_row[sheet] = 1
82
- @last_row[sheet] = 0
83
- @first_column[sheet] = 1
84
- @last_column[sheet] = 1
85
- rownum = 1
86
- each_row csv_options do |row|
87
- row.each_with_index do |elem,i|
88
- @cell[[rownum,i+1]] = cell_postprocessing rownum,i+1, elem
89
- @cell_type[[rownum,i+1]] = celltype_class @cell[[rownum,i+1]]
90
- if i+1 > @last_column[sheet]
91
- @last_column[sheet] += 1
92
- end
100
+ def each_row_using_tempdir(options, &block)
101
+ ::Dir.mktmpdir(Roo::TEMP_PREFIX, ENV["ROO_TMP"]) do |tmpdir|
102
+ tmp_filename = download_uri(filename, tmpdir)
103
+ ::CSV.foreach(tmp_filename, options, &block)
93
104
  end
94
- rownum += 1
95
- @last_row[sheet] += 1
96
- end
97
- @cells_read[sheet] = true
98
- #-- adjust @first_row if neccessary
99
- while !row(@first_row[sheet]).any? and @first_row[sheet] < @last_row[sheet]
100
- @first_row[sheet] += 1
101
- end
102
- #-- adjust @last_row if neccessary
103
- while !row(@last_row[sheet]).any? and @last_row[sheet] and
104
- @last_row[sheet] > @first_row[sheet]
105
- @last_row[sheet] -= 1
106
105
  end
107
- #-- adjust @first_column if neccessary
108
- while !column(@first_column[sheet]).any? and
109
- @first_column[sheet] and
110
- @first_column[sheet] < @last_column[sheet]
111
- @first_column[sheet] += 1
106
+
107
+ def set_row_count(sheet, last_row)
108
+ @first_row[sheet] = 1
109
+ @last_row[sheet] = last_row
110
+ @last_row[sheet] = @first_row[sheet] if @last_row[sheet].zero?
111
+
112
+ nil
112
113
  end
113
- #-- adjust @last_column if neccessary
114
- while !column(@last_column[sheet]).any? and
115
- @last_column[sheet] and
116
- @last_column[sheet] > @first_column[sheet]
117
- @last_column[sheet] -= 1
114
+
115
+ def set_column_count(sheet, last_col)
116
+ @first_column[sheet] = 1
117
+ @last_column[sheet] = last_col
118
+ @last_column[sheet] = @first_column[sheet] if @last_column[sheet].zero?
119
+
120
+ nil
118
121
  end
119
- end
120
122
 
121
- def clean_sheet(sheet)
122
- read_cells(sheet)
123
+ def clean_sheet(sheet)
124
+ read_cells(sheet)
125
+
126
+ @cell.each_pair do |coord, value|
127
+ @cell[coord] = sanitize_value(value) if value.is_a?(::String)
128
+ end
123
129
 
124
- @cell.each_pair do |coord, value|
125
- @cell[coord] = sanitize_value(value) if value.is_a?(::String)
130
+ @cleaned[sheet] = true
126
131
  end
127
132
 
128
- @cleaned[sheet] = true
133
+ alias_method :filename_or_stream, :filename
129
134
  end
130
-
131
- alias_method :filename_or_stream, :filename
132
135
  end
@@ -1,13 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roo/helpers/default_attr_reader"
4
+
1
5
  module Roo
2
6
  class Excelx
3
7
  class Cell
4
8
  class Base
9
+ extend Roo::Helpers::DefaultAttrReader
5
10
  attr_reader :cell_type, :cell_value, :value
6
11
 
7
12
  # FIXME: I think style should be deprecated. Having a style attribute
8
13
  # for a cell doesn't really accomplish much. It seems to be used
9
14
  # when you want to export to excelx.
10
- attr_reader :style
15
+ attr_reader_with_default default_type: :base, style: 1
11
16
 
12
17
 
13
18
  # FIXME: Updating a cell's value should be able tochange the cell's type,
@@ -34,14 +39,12 @@ module Roo
34
39
  attr_writer :value
35
40
 
36
41
  def initialize(value, formula, excelx_type, style, link, coordinate)
37
- @link = !!link
38
42
  @cell_value = value
39
- @cell_type = excelx_type
40
- @formula = formula
41
- @style = style
43
+ @cell_type = excelx_type if excelx_type
44
+ @formula = formula if formula
45
+ @style = style unless style == 1
42
46
  @coordinate = coordinate
43
- @type = :base
44
- @value = link? ? Roo::Link.new(link, value) : value
47
+ @value = link ? Roo::Link.new(link, value) : value
45
48
  end
46
49
 
47
50
  def type
@@ -50,16 +53,16 @@ module Roo
50
53
  elsif link?
51
54
  :link
52
55
  else
53
- @type
56
+ default_type
54
57
  end
55
58
  end
56
59
 
57
60
  def formula?
58
- !!@formula
61
+ !!(defined?(@formula) && @formula)
59
62
  end
60
63
 
61
64
  def link?
62
- !!@link
65
+ Roo::Link === @value
63
66
  end
64
67
 
65
68
  alias_method :formatted_value, :value
@@ -68,9 +71,16 @@ module Roo
68
71
  formatted_value
69
72
  end
70
73
 
71
- # DEPRECATED: Please use link instead.
74
+ # DEPRECATED: Please use link? instead.
72
75
  def hyperlink
73
- warn '[DEPRECATION] `hyperlink` is deprecated. Please use `link` instead.'
76
+ warn '[DEPRECATION] `hyperlink` is deprecated. Please use `link?` instead.'
77
+ link?
78
+ end
79
+
80
+ # DEPRECATED: Please use link? instead.
81
+ def link
82
+ warn '[DEPRECATION] `link` is deprecated. Please use `link?` instead.'
83
+ link?
74
84
  end
75
85
 
76
86
  # DEPRECATED: Please use cell_value instead.
@@ -88,6 +98,10 @@ module Roo
88
98
  def empty?
89
99
  false
90
100
  end
101
+
102
+ def presence
103
+ empty? ? nil : self
104
+ end
91
105
  end
92
106
  end
93
107
  end
@@ -1,17 +1,20 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Roo
2
4
  class Excelx
3
5
  class Cell
4
6
  class Boolean < Cell::Base
5
- attr_reader :value, :formula, :format, :cell_type, :cell_value, :link, :coordinate
7
+ attr_reader :value, :formula, :format, :cell_value, :coordinate
8
+
9
+ attr_reader_with_default default_type: :boolean, cell_type: :boolean
6
10
 
7
11
  def initialize(value, formula, style, link, coordinate)
8
- super(value, formula, nil, style, link, coordinate)
9
- @type = @cell_type = :boolean
10
- @value = link? ? Roo::Link.new(link, value) : create_boolean(value)
12
+ super(value, formula, nil, style, nil, coordinate)
13
+ @value = link ? Roo::Link.new(link, value) : create_boolean(value)
11
14
  end
12
15
 
13
16
  def formatted_value
14
- value ? 'TRUE'.freeze : 'FALSE'.freeze
17
+ value ? 'TRUE' : 'FALSE'
15
18
  end
16
19
 
17
20
  private
@@ -19,7 +22,7 @@ module Roo
19
22
  def create_boolean(value)
20
23
  # FIXME: Using a boolean will cause methods like Base#to_csv to fail.
21
24
  # Roo is using some method to ignore false/nil values.
22
- value.to_i == 1 ? true : false
25
+ value.to_i == 1
23
26
  end
24
27
  end
25
28
  end
@@ -4,23 +4,23 @@ module Roo
4
4
  class Excelx
5
5
  class Cell
6
6
  class Date < Roo::Excelx::Cell::DateTime
7
- attr_reader :value, :formula, :format, :cell_type, :cell_value, :link, :coordinate
7
+ attr_reader :value, :formula, :format, :cell_type, :cell_value, :coordinate
8
+
9
+ attr_reader_with_default default_type: :date
8
10
 
9
11
  def initialize(value, formula, excelx_type, style, link, base_date, coordinate)
10
12
  # NOTE: Pass all arguments to the parent class, DateTime.
11
13
  super
12
- @type = :date
13
14
  @format = excelx_type.last
14
- @value = link? ? Roo::Link.new(link, value) : create_date(base_date, value)
15
+ @value = link ? Roo::Link.new(link, value) : create_date(base_date, value)
15
16
  end
16
17
 
17
18
  private
18
19
 
19
- def create_date(base_date, value)
20
- date = base_date + value.to_i
21
- yyyy, mm, dd = date.strftime('%Y-%m-%d').split('-')
20
+ def create_datetime(_,_); end
22
21
 
23
- ::Date.new(yyyy.to_i, mm.to_i, dd.to_i)
22
+ def create_date(base_date, value)
23
+ base_date + value.to_i
24
24
  end
25
25
  end
26
26
  end
@@ -1,16 +1,21 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'date'
2
4
 
3
5
  module Roo
4
6
  class Excelx
5
7
  class Cell
6
8
  class DateTime < Cell::Base
7
- attr_reader :value, :formula, :format, :cell_value, :link, :coordinate
9
+ SECONDS_IN_DAY = 60 * 60 * 24
10
+
11
+ attr_reader :value, :formula, :format, :cell_value, :coordinate
8
12
 
9
- def initialize(value, formula, excelx_type, style, link, base_date, coordinate)
10
- super(value, formula, excelx_type, style, link, coordinate)
11
- @type = :datetime
13
+ attr_reader_with_default default_type: :datetime
14
+
15
+ def initialize(value, formula, excelx_type, style, link, base_timestamp, coordinate)
16
+ super(value, formula, excelx_type, style, nil, coordinate)
12
17
  @format = excelx_type.last
13
- @value = link? ? Roo::Link.new(link, value) : create_datetime(base_date, value)
18
+ @value = link ? Roo::Link.new(link, value) : create_datetime(base_timestamp, value)
14
19
  end
15
20
 
16
21
  # Public: Returns formatted value for a datetime. Format's can be an
@@ -32,14 +37,9 @@ module Roo
32
37
  #
33
38
  # Returns a String representation of a cell's value.
34
39
  def formatted_value
35
- date_regex = /(?<date>[dmy]+[\-\/][dmy]+([\-\/][dmy]+)?)/
36
- time_regex = /(?<time>(\[?[h]\]?+:)?[m]+(:?ss|:?s)?)/
37
-
38
40
  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)
41
+ if (parsed_format = parse_date_or_time_format(part))
42
+ parsed_format
43
43
  else
44
44
  warn 'Unable to parse custom format. Using "YYYY-mm-dd HH:MM:SS" format.'
45
45
  return @value.strftime('%F %T')
@@ -51,49 +51,55 @@ module Roo
51
51
 
52
52
  private
53
53
 
54
+ def parse_date_or_time_format(part)
55
+ date_regex = /(?<date>[dmy]+[\-\/][dmy]+([\-\/][dmy]+)?)/
56
+ time_regex = /(?<time>(\[?[h]\]?+:)?[m]+(:?ss|:?s)?)/
57
+
58
+ if part[date_regex] == part
59
+ formats = DATE_FORMATS
60
+ elsif part[time_regex]
61
+ formats = TIME_FORMATS
62
+ else
63
+ return false
64
+ end
65
+
66
+ part.gsub(/#{formats.keys.join('|')}/, formats)
67
+ end
68
+
54
69
  DATE_FORMATS = {
55
- 'yyyy'.freeze => '%Y'.freeze, # Year: 2000
56
- 'yy'.freeze => '%y'.freeze, # Year: 00
70
+ 'yyyy' => '%Y', # Year: 2000
71
+ 'yy' => '%y', # Year: 00
57
72
  # 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
73
+ 'mmmm' => '%B', # Month: January
74
+ 'mmm' => '%^b', # Month: JAN
75
+ 'mm' => '%m', # Month: 01
76
+ 'm' => '%-m', # Month: 1
77
+ 'dddd' => '%A', # Day of the Week: Sunday
78
+ 'ddd' => '%^a', # Day of the Week: SUN
79
+ 'dd' => '%d', # Day of the Month: 01
80
+ 'd' => '%-d' # Day of the Month: 1
66
81
  # '\\\\'.freeze => ''.freeze, # NOTE: Fixes a custom format's output.
67
82
  }
68
83
 
69
84
  TIME_FORMATS = {
70
- 'hh'.freeze => '%H'.freeze, # Hour (24): 01
71
- 'h'.freeze => '%-k'.freeze, # Hour (24): 1
85
+ 'hh' => '%H', # Hour (24): 01
86
+ 'h' => '%-k', # Hour (24): 1
72
87
  # 'hh'.freeze => '%I'.freeze, # Hour (12): 08
73
88
  # 'h'.freeze => '%-l'.freeze, # Hour (12): 8
74
- 'mm'.freeze => '%M'.freeze, # Minute: 01
89
+ 'mm' => '%M', # Minute: 01
75
90
  # 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.
91
+ 'm' => '%-M', # Minute: 1
92
+ 'ss' => '%S', # Seconds: 01
93
+ 's' => '%-S', # Seconds: 1
94
+ 'am/pm' => '%p', # Meridian: AM
95
+ '000' => '%3N', # Fractional Seconds: thousandth.
96
+ '00' => '%2N', # Fractional Seconds: hundredth.
97
+ '0' => '%1N' # Fractional Seconds: tenths.
83
98
  }
84
99
 
85
- def create_datetime(base_date, value)
86
- date = base_date + value.to_f.round(6)
87
- datetime_string = date.strftime('%Y-%m-%d %H:%M:%S.%N')
88
- t = round_datetime(datetime_string)
89
-
90
- ::DateTime.civil(t.year, t.month, t.day, t.hour, t.min, t.sec)
91
- end
92
-
93
- def round_datetime(datetime_string)
94
- /(?<yyyy>\d+)-(?<mm>\d+)-(?<dd>\d+) (?<hh>\d+):(?<mi>\d+):(?<ss>\d+.\d+)/ =~ datetime_string
95
-
96
- ::Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0)
100
+ def create_datetime(base_timestamp, value)
101
+ timestamp = (base_timestamp + (value.to_f.round(6) * SECONDS_IN_DAY)).round(0)
102
+ ::Time.at(timestamp).utc.to_datetime
97
103
  end
98
104
  end
99
105
  end
@@ -3,10 +3,11 @@ module Roo
3
3
  class Excelx
4
4
  class Cell
5
5
  class Empty < Cell::Base
6
- attr_reader :value, :formula, :format, :cell_type, :cell_value, :hyperlink, :coordinate
6
+ attr_reader :value, :formula, :format, :cell_type, :cell_value, :coordinate
7
+
8
+ attr_reader_with_default default_type: nil, style: nil
7
9
 
8
10
  def initialize(coordinate)
9
- @value = @formula = @format = @cell_type = @cell_value = @hyperlink = nil
10
11
  @coordinate = coordinate
11
12
  end
12
13
 
@@ -1,16 +1,19 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Roo
2
4
  class Excelx
3
5
  class Cell
4
6
  class Number < Cell::Base
5
- attr_reader :value, :formula, :format, :cell_value, :link, :coordinate
7
+ attr_reader :value, :formula, :format, :cell_value, :coordinate
8
+
9
+ # FIXME: change default_type to number. This will break brittle tests.
10
+ attr_reader_with_default default_type: :float
6
11
 
7
12
  def initialize(value, formula, excelx_type, style, link, coordinate)
8
13
  super
9
- # FIXME: change @type to number. This will break brittle tests.
10
14
  # FIXME: Excelx_type is an array, but the first value isn't used.
11
- @type = :float
12
15
  @format = excelx_type.last
13
- @value = link? ? Roo::Link.new(link, value) : create_numeric(value)
16
+ @value = link ? Roo::Link.new(link, value) : create_numeric(value)
14
17
  end
15
18
 
16
19
  def create_numeric(number)
@@ -21,69 +24,63 @@ module Roo
21
24
  when /\.0/
22
25
  Float(number)
23
26
  else
24
- (number.include?('.') || (/\A[-+]?\d+E[-+]\d+\z/i =~ number)) ? Float(number) : Integer(number)
27
+ (number.include?('.') || (/\A[-+]?\d+E[-+]?\d+\z/i =~ number)) ? Float(number) : Integer(number, 10)
25
28
  end
26
29
  end
27
30
 
28
31
  def formatted_value
29
32
  return @cell_value if Excelx::ERROR_VALUES.include?(@cell_value)
30
33
 
31
- formatter = formats[@format]
34
+ formatter = generate_formatter(@format)
32
35
  if formatter.is_a? Proc
33
36
  formatter.call(@cell_value)
34
- elsif zero_padded_number?
35
- "%0#{@format.size}d"% @cell_value
36
37
  else
37
38
  Kernel.format(formatter, @cell_value)
38
39
  end
39
40
  end
40
41
 
41
- def formats
42
+ def generate_formatter(format)
42
43
  # FIXME: numbers can be other colors besides red:
43
44
  # [BLACK], [BLUE], [CYAN], [GREEN], [MAGENTA], [RED], [WHITE], [YELLOW], [COLOR n]
44
- {
45
- 'General' => '%.0f',
46
- '0' => '%.0f',
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,
54
- '0%' => proc do |number|
55
- Kernel.format('%d%', number.to_f * 100)
56
- end,
57
- '0.00%' => proc do |number|
58
- Kernel.format('%.2f%', number.to_f * 100)
59
- end,
60
- '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,
45
+ case format
46
+ when /^General$/i then '%.0f'
47
+ when '0' then '%.0f'
48
+ when /^(0+)$/ then "%0#{$1.size}d"
49
+ when /^0\.(0+)$/ then "%.#{$1.size}f"
50
+ when '#,##0' then number_format('%.0f')
51
+ when '#,##0.00' then number_format('%.2f')
52
+ when '0%'
53
+ proc do |number|
54
+ Kernel.format('%d%%', number.to_f * 100)
55
+ end
56
+ when '0.00%'
57
+ proc do |number|
58
+ Kernel.format('%.2f%%', number.to_f * 100)
59
+ end
60
+ when '0.00E+00' then '%.2E'
61
+ when '#,##0 ;(#,##0)' then number_format('%.0f', '(%.0f)')
62
+ when '#,##0 ;[Red](#,##0)' then number_format('%.0f', '[Red](%.0f)')
63
+ when '#,##0.00;(#,##0.00)' then number_format('%.2f', '(%.2f)')
64
+ when '#,##0.00;[Red](#,##0.00)' then number_format('%.2f', '[Red](%.2f)')
77
65
  # FIXME: not quite sure what the format should look like in this case.
78
- '##0.0E+0' => '%.1E',
79
- '@' => proc { |number| number }
80
- }
66
+ when '##0.0E+0' then '%.1E'
67
+ when '@' then proc { |number| number }
68
+ else
69
+ raise "Unknown format: #{format.inspect}"
70
+ end
81
71
  end
82
72
 
83
73
  private
84
74
 
85
- def zero_padded_number?
86
- @format[/0+/] == @format
75
+ def number_format(formatter, negative_formatter = nil)
76
+ proc do |number|
77
+ if negative_formatter
78
+ formatter = number.to_i > 0 ? formatter : negative_formatter
79
+ number = number.to_f.abs
80
+ end
81
+
82
+ Kernel.format(formatter, number).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
83
+ end
87
84
  end
88
85
  end
89
86
  end