roo 2.6.0 → 2.8.3

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 (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