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.
- checksums.yaml +5 -5
- data/.codeclimate.yml +17 -0
- data/.github/issue_template.md +16 -0
- data/.github/pull_request_template.md +14 -0
- data/.rubocop.yml +186 -0
- data/.travis.yml +14 -11
- data/CHANGELOG.md +64 -2
- data/Gemfile +2 -4
- data/LICENSE +2 -0
- data/README.md +36 -10
- data/lib/roo/base.rb +82 -225
- data/lib/roo/constants.rb +5 -3
- data/lib/roo/csv.rb +100 -97
- data/lib/roo/excelx/cell/base.rb +26 -12
- data/lib/roo/excelx/cell/boolean.rb +9 -6
- data/lib/roo/excelx/cell/date.rb +7 -7
- data/lib/roo/excelx/cell/datetime.rb +50 -44
- data/lib/roo/excelx/cell/empty.rb +3 -2
- data/lib/roo/excelx/cell/number.rb +44 -47
- data/lib/roo/excelx/cell/string.rb +3 -3
- data/lib/roo/excelx/cell/time.rb +17 -16
- data/lib/roo/excelx/cell.rb +10 -6
- data/lib/roo/excelx/comments.rb +3 -3
- data/lib/roo/excelx/coordinate.rb +11 -4
- data/lib/roo/excelx/extractor.rb +21 -3
- data/lib/roo/excelx/format.rb +38 -31
- data/lib/roo/excelx/images.rb +26 -0
- data/lib/roo/excelx/relationships.rb +12 -4
- data/lib/roo/excelx/shared.rb +10 -3
- data/lib/roo/excelx/shared_strings.rb +9 -15
- data/lib/roo/excelx/sheet.rb +49 -10
- data/lib/roo/excelx/sheet_doc.rb +89 -48
- data/lib/roo/excelx/styles.rb +3 -3
- data/lib/roo/excelx/workbook.rb +7 -3
- data/lib/roo/excelx.rb +50 -19
- data/lib/roo/formatters/base.rb +15 -0
- data/lib/roo/formatters/csv.rb +84 -0
- data/lib/roo/formatters/matrix.rb +23 -0
- data/lib/roo/formatters/xml.rb +31 -0
- data/lib/roo/formatters/yaml.rb +40 -0
- data/lib/roo/helpers/default_attr_reader.rb +20 -0
- data/lib/roo/helpers/weak_instance_cache.rb +41 -0
- data/lib/roo/open_office.rb +17 -9
- data/lib/roo/spreadsheet.rb +1 -1
- data/lib/roo/tempdir.rb +5 -10
- data/lib/roo/utils.rb +70 -20
- data/lib/roo/version.rb +1 -1
- data/lib/roo.rb +4 -1
- data/roo.gemspec +14 -11
- data/spec/lib/roo/base_spec.rb +45 -3
- data/spec/lib/roo/excelx/relationships_spec.rb +43 -0
- data/spec/lib/roo/excelx/sheet_doc_spec.rb +11 -0
- data/spec/lib/roo/excelx_spec.rb +150 -31
- data/spec/lib/roo/strict_spec.rb +43 -0
- data/spec/lib/roo/utils_spec.rb +25 -3
- data/spec/lib/roo/weak_instance_cache_spec.rb +92 -0
- data/spec/lib/roo_spec.rb +0 -0
- data/spec/spec_helper.rb +2 -6
- data/test/excelx/cell/test_attr_reader_default.rb +72 -0
- data/test/excelx/cell/test_base.rb +5 -0
- data/test/excelx/cell/test_datetime.rb +6 -6
- data/test/excelx/cell/test_empty.rb +11 -0
- data/test/excelx/cell/test_number.rb +9 -0
- data/test/excelx/cell/test_string.rb +20 -0
- data/test/excelx/cell/test_time.rb +5 -5
- data/test/excelx/test_coordinate.rb +51 -0
- data/test/formatters/test_csv.rb +136 -0
- data/test/formatters/test_matrix.rb +76 -0
- data/test/formatters/test_xml.rb +78 -0
- data/test/formatters/test_yaml.rb +20 -0
- data/test/helpers/test_accessing_files.rb +60 -0
- data/test/helpers/test_comments.rb +43 -0
- data/test/helpers/test_formulas.rb +9 -0
- data/test/helpers/test_labels.rb +103 -0
- data/test/helpers/test_sheets.rb +55 -0
- data/test/helpers/test_styles.rb +62 -0
- data/test/roo/test_base.rb +182 -0
- data/test/roo/test_csv.rb +88 -0
- data/test/roo/test_excelx.rb +330 -0
- data/test/roo/test_libre_office.rb +9 -0
- data/test/roo/test_open_office.rb +289 -0
- data/test/test_helper.rb +129 -14
- data/test/test_roo.rb +32 -1787
- metadata +81 -29
- data/.github/ISSUE_TEMPLATE +0 -10
- data/Gemfile_ruby2 +0 -29
data/lib/roo/csv.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
-
|
2
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
19
|
-
|
20
|
-
|
21
|
-
['default']
|
22
|
-
end
|
36
|
+
def cell_postprocessing(_row, _col, value)
|
37
|
+
value
|
38
|
+
end
|
23
39
|
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
@cell[normalize(row,col)]
|
28
|
-
end
|
40
|
+
def csv_options
|
41
|
+
@options[:csv_options] || {}
|
42
|
+
end
|
29
43
|
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
37
|
-
|
38
|
-
|
48
|
+
def set_type(row, col, type, _sheet)
|
49
|
+
@cell_type[[row, col]] = type
|
50
|
+
end
|
39
51
|
|
40
|
-
|
41
|
-
@options[:csv_options] || {}
|
42
|
-
end
|
52
|
+
private
|
43
53
|
|
44
|
-
|
45
|
-
|
46
|
-
|
54
|
+
TYPE_MAP = {
|
55
|
+
String => :string,
|
56
|
+
Float => :float,
|
57
|
+
Date => :date,
|
58
|
+
DateTime => :datetime,
|
59
|
+
}
|
47
60
|
|
48
|
-
|
49
|
-
|
50
|
-
|
61
|
+
def celltype_class(value)
|
62
|
+
TYPE_MAP[value.class]
|
63
|
+
end
|
51
64
|
|
52
|
-
|
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
|
-
|
55
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
CSV.
|
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
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
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
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
@
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
@last_column[sheet]
|
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
|
-
|
122
|
-
|
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
|
-
|
125
|
-
@cell[coord] = sanitize_value(value) if value.is_a?(::String)
|
130
|
+
@cleaned[sheet] = true
|
126
131
|
end
|
127
132
|
|
128
|
-
|
133
|
+
alias_method :filename_or_stream, :filename
|
129
134
|
end
|
130
|
-
|
131
|
-
alias_method :filename_or_stream, :filename
|
132
135
|
end
|
data/lib/roo/excelx/cell/base.rb
CHANGED
@@ -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
|
-
|
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
|
-
@
|
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
|
-
|
56
|
+
default_type
|
54
57
|
end
|
55
58
|
end
|
56
59
|
|
57
60
|
def formula?
|
58
|
-
|
61
|
+
!!(defined?(@formula) && @formula)
|
59
62
|
end
|
60
63
|
|
61
64
|
def link?
|
62
|
-
|
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
|
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, :
|
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,
|
9
|
-
@
|
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'
|
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
|
25
|
+
value.to_i == 1
|
23
26
|
end
|
24
27
|
end
|
25
28
|
end
|
data/lib/roo/excelx/cell/date.rb
CHANGED
@@ -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, :
|
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
|
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
|
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
|
-
|
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
|
-
|
9
|
+
SECONDS_IN_DAY = 60 * 60 * 24
|
10
|
+
|
11
|
+
attr_reader :value, :formula, :format, :cell_value, :coordinate
|
8
12
|
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
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
|
40
|
-
|
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'
|
56
|
-
'yy'
|
70
|
+
'yyyy' => '%Y', # Year: 2000
|
71
|
+
'yy' => '%y', # Year: 00
|
57
72
|
# mmmmm => J-D
|
58
|
-
'mmmm'
|
59
|
-
'mmm'
|
60
|
-
'mm'
|
61
|
-
'm'
|
62
|
-
'dddd'
|
63
|
-
'ddd'
|
64
|
-
'dd'
|
65
|
-
'd'
|
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'
|
71
|
-
'h'
|
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'
|
89
|
+
'mm' => '%M', # Minute: 01
|
75
90
|
# FIXME: is this used? Seems like 'm' is used for month, not minute.
|
76
|
-
'm'
|
77
|
-
'ss'
|
78
|
-
's'
|
79
|
-
'am/pm'
|
80
|
-
'000'
|
81
|
-
'00'
|
82
|
-
'0'
|
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(
|
86
|
-
|
87
|
-
|
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, :
|
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, :
|
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
|
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[-+]
|
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 =
|
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
|
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
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
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
|
-
|
79
|
-
|
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
|
86
|
-
|
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
|