roo 2.0.1 → 2.7.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (84) hide show
  1. checksums.yaml +4 -4
  2. data/.codeclimate.yml +17 -0
  3. data/.github/ISSUE_TEMPLATE +10 -0
  4. data/.gitignore +4 -0
  5. data/.travis.yml +10 -6
  6. data/CHANGELOG.md +116 -1
  7. data/Gemfile +3 -4
  8. data/Gemfile_ruby2 +30 -0
  9. data/Guardfile +1 -2
  10. data/README.md +56 -22
  11. data/Rakefile +1 -1
  12. data/lib/roo/base.rb +108 -245
  13. data/lib/roo/constants.rb +5 -0
  14. data/lib/roo/csv.rb +94 -87
  15. data/lib/roo/errors.rb +11 -0
  16. data/lib/roo/excelx/cell/base.rb +94 -0
  17. data/lib/roo/excelx/cell/boolean.rb +27 -0
  18. data/lib/roo/excelx/cell/date.rb +28 -0
  19. data/lib/roo/excelx/cell/datetime.rb +111 -0
  20. data/lib/roo/excelx/cell/empty.rb +19 -0
  21. data/lib/roo/excelx/cell/number.rb +87 -0
  22. data/lib/roo/excelx/cell/string.rb +19 -0
  23. data/lib/roo/excelx/cell/time.rb +43 -0
  24. data/lib/roo/excelx/cell.rb +33 -4
  25. data/lib/roo/excelx/comments.rb +33 -0
  26. data/lib/roo/excelx/coordinate.rb +12 -0
  27. data/lib/roo/excelx/extractor.rb +3 -4
  28. data/lib/roo/excelx/format.rb +64 -0
  29. data/lib/roo/excelx/shared.rb +32 -0
  30. data/lib/roo/excelx/shared_strings.rb +124 -4
  31. data/lib/roo/excelx/sheet.rb +12 -7
  32. data/lib/roo/excelx/sheet_doc.rb +108 -97
  33. data/lib/roo/excelx/styles.rb +1 -1
  34. data/lib/roo/excelx.rb +61 -103
  35. data/lib/roo/formatters/base.rb +15 -0
  36. data/lib/roo/formatters/csv.rb +84 -0
  37. data/lib/roo/formatters/matrix.rb +23 -0
  38. data/lib/roo/formatters/xml.rb +31 -0
  39. data/lib/roo/formatters/yaml.rb +40 -0
  40. data/lib/roo/libre_office.rb +1 -2
  41. data/lib/roo/link.rb +21 -2
  42. data/lib/roo/open_office.rb +468 -523
  43. data/lib/roo/spreadsheet.rb +3 -1
  44. data/lib/roo/tempdir.rb +21 -0
  45. data/lib/roo/utils.rb +7 -7
  46. data/lib/roo/version.rb +1 -1
  47. data/lib/roo.rb +8 -3
  48. data/roo.gemspec +2 -1
  49. data/spec/helpers.rb +5 -0
  50. data/spec/lib/roo/base_spec.rb +229 -0
  51. data/spec/lib/roo/csv_spec.rb +19 -0
  52. data/spec/lib/roo/excelx_spec.rb +97 -11
  53. data/spec/lib/roo/openoffice_spec.rb +18 -1
  54. data/spec/lib/roo/spreadsheet_spec.rb +20 -0
  55. data/spec/lib/roo/utils_spec.rb +1 -1
  56. data/spec/spec_helper.rb +5 -5
  57. data/test/all_ss.rb +12 -11
  58. data/test/excelx/cell/test_base.rb +63 -0
  59. data/test/excelx/cell/test_boolean.rb +36 -0
  60. data/test/excelx/cell/test_date.rb +38 -0
  61. data/test/excelx/cell/test_datetime.rb +45 -0
  62. data/test/excelx/cell/test_empty.rb +7 -0
  63. data/test/excelx/cell/test_number.rb +74 -0
  64. data/test/excelx/cell/test_string.rb +28 -0
  65. data/test/excelx/cell/test_time.rb +30 -0
  66. data/test/formatters/test_csv.rb +119 -0
  67. data/test/formatters/test_matrix.rb +76 -0
  68. data/test/formatters/test_xml.rb +78 -0
  69. data/test/formatters/test_yaml.rb +20 -0
  70. data/test/helpers/test_accessing_files.rb +60 -0
  71. data/test/helpers/test_comments.rb +43 -0
  72. data/test/helpers/test_formulas.rb +9 -0
  73. data/test/helpers/test_labels.rb +103 -0
  74. data/test/helpers/test_sheets.rb +55 -0
  75. data/test/helpers/test_styles.rb +62 -0
  76. data/test/roo/test_base.rb +182 -0
  77. data/test/roo/test_csv.rb +60 -0
  78. data/test/roo/test_excelx.rb +325 -0
  79. data/test/roo/test_libre_office.rb +9 -0
  80. data/test/roo/test_open_office.rb +289 -0
  81. data/test/test_helper.rb +116 -18
  82. data/test/test_roo.rb +362 -2088
  83. metadata +70 -4
  84. data/test/test_generic_spreadsheet.rb +0 -237
data/lib/roo/csv.rb CHANGED
@@ -1,5 +1,5 @@
1
- require 'csv'
2
- require 'time'
1
+ require "csv"
2
+ require "time"
3
3
 
4
4
  # The CSV class can read csv files (must be separated with commas) which then
5
5
  # can be handled like spreadsheets. This means you can access cells like A5
@@ -9,112 +9,119 @@ require 'time'
9
9
  #
10
10
  # You can pass options to the underlying CSV parse operation, via the
11
11
  # :csv_options option.
12
- #
13
-
14
- class Roo::CSV < Roo::Base
15
-
16
- attr_reader :filename
12
+ module Roo
13
+ class CSV < Roo::Base
14
+ attr_reader :filename
15
+
16
+ # Returns an array with the names of the sheets. In CSV class there is only
17
+ # one dummy sheet, because a csv file cannot have more than one sheet.
18
+ def sheets
19
+ ["default"]
20
+ end
17
21
 
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
22
+ def cell(row, col, sheet = nil)
23
+ sheet ||= default_sheet
24
+ read_cells(sheet)
25
+ @cell[normalize(row, col)]
26
+ end
23
27
 
24
- def cell(row, col, sheet=nil)
25
- sheet ||= default_sheet
26
- read_cells(sheet)
27
- @cell[normalize(row,col)]
28
- end
28
+ def celltype(row, col, sheet = nil)
29
+ sheet ||= default_sheet
30
+ read_cells(sheet)
31
+ @cell_type[normalize(row, col)]
32
+ end
29
33
 
30
- def celltype(row, col, sheet=nil)
31
- sheet ||= default_sheet
32
- read_cells(sheet)
33
- @cell_type[normalize(row,col)]
34
- end
34
+ def cell_postprocessing(_row, _col, value)
35
+ value
36
+ end
35
37
 
36
- def cell_postprocessing(row,col,value)
37
- value
38
- end
38
+ def csv_options
39
+ @options[:csv_options] || {}
40
+ end
39
41
 
40
- def csv_options
41
- @options[:csv_options] || {}
42
- end
42
+ def set_value(row, col, value, _sheet)
43
+ @cell[[row, col]] = value
44
+ end
43
45
 
44
- private
46
+ def set_type(row, col, type, _sheet)
47
+ @cell_type[[row, col]] = type
48
+ end
45
49
 
46
- TYPE_MAP = {
47
- String => :string,
48
- Float => :float,
49
- Date => :date,
50
- DateTime => :datetime,
51
- }
50
+ private
52
51
 
53
- def celltype_class(value)
54
- TYPE_MAP[value.class]
55
- end
52
+ TYPE_MAP = {
53
+ String => :string,
54
+ Float => :float,
55
+ Date => :date,
56
+ DateTime => :datetime,
57
+ }
56
58
 
57
- def each_row(options, &block)
58
- if uri?(filename)
59
- make_tmpdir do |tmpdir|
60
- tmp_filename = download_uri(filename, tmpdir)
61
- CSV.foreach(tmp_filename, options, &block)
62
- end
63
- else
64
- CSV.foreach(filename, options, &block)
59
+ def celltype_class(value)
60
+ TYPE_MAP[value.class]
65
61
  end
66
- end
67
62
 
68
- def read_cells(sheet = default_sheet)
69
- sheet ||= default_sheet
70
- return if @cells_read[sheet]
71
- @first_row[sheet] = 1
72
- @last_row[sheet] = 0
73
- @first_column[sheet] = 1
74
- @last_column[sheet] = 1
75
- rownum = 1
76
- each_row csv_options do |row|
77
- row.each_with_index do |elem,i|
78
- @cell[[rownum,i+1]] = cell_postprocessing rownum,i+1, elem
79
- @cell_type[[rownum,i+1]] = celltype_class @cell[[rownum,i+1]]
80
- if i+1 > @last_column[sheet]
81
- @last_column[sheet] += 1
63
+ def read_cells(sheet = default_sheet)
64
+ sheet ||= default_sheet
65
+ return if @cells_read[sheet]
66
+ set_row_count(sheet)
67
+ set_column_count(sheet)
68
+ row_num = 1
69
+
70
+ each_row csv_options do |row|
71
+ row.each_with_index do |elem, col_num|
72
+ coordinate = [row_num, col_num + 1]
73
+ @cell[coordinate] = elem
74
+ @cell_type[coordinate] = celltype_class(elem)
82
75
  end
76
+ row_num += 1
83
77
  end
84
- rownum += 1
85
- @last_row[sheet] += 1
78
+
79
+ @cells_read[sheet] = true
86
80
  end
87
- @cells_read[sheet] = true
88
- #-- adjust @first_row if neccessary
89
- while !row(@first_row[sheet]).any? and @first_row[sheet] < @last_row[sheet]
90
- @first_row[sheet] += 1
81
+
82
+ def each_row(options, &block)
83
+ if uri?(filename)
84
+ each_row_using_temp_dir(filename)
85
+ elsif is_stream?(filename_or_stream)
86
+ ::CSV.new(filename_or_stream, options).each(&block)
87
+ else
88
+ ::CSV.foreach(filename, options, &block)
89
+ end
91
90
  end
92
- #-- adjust @last_row if neccessary
93
- while !row(@last_row[sheet]).any? and @last_row[sheet] and
94
- @last_row[sheet] > @first_row[sheet]
95
- @last_row[sheet] -= 1
91
+
92
+ def each_row_using_tempdir
93
+ ::Dir.mktmpdir(Roo::TEMP_PREFIX, ENV["ROO_TMP"]) do |tmpdir|
94
+ tmp_filename = download_uri(filename, tmpdir)
95
+ ::CSV.foreach(tmp_filename, options, &block)
96
+ end
96
97
  end
97
- #-- adjust @first_column if neccessary
98
- while !column(@first_column[sheet]).any? and
99
- @first_column[sheet] and
100
- @first_column[sheet] < @last_column[sheet]
101
- @first_column[sheet] += 1
98
+
99
+ def set_row_count(sheet)
100
+ @first_row[sheet] = 1
101
+ @last_row[sheet] = ::CSV.readlines(@filename, csv_options).size
102
+ @last_row[sheet] = @first_row[sheet] if @last_row[sheet].zero?
103
+
104
+ nil
102
105
  end
103
- #-- adjust @last_column if neccessary
104
- while !column(@last_column[sheet]).any? and
105
- @last_column[sheet] and
106
- @last_column[sheet] > @first_column[sheet]
107
- @last_column[sheet] -= 1
106
+
107
+ def set_column_count(sheet)
108
+ @first_column[sheet] = 1
109
+ @last_column[sheet] = (::CSV.readlines(@filename, csv_options).first || []).size
110
+ @last_column[sheet] = @first_column[sheet] if @last_column[sheet].zero?
111
+
112
+ nil
108
113
  end
109
- end
110
114
 
111
- def clean_sheet(sheet)
112
- read_cells(sheet)
115
+ def clean_sheet(sheet)
116
+ read_cells(sheet)
117
+
118
+ @cell.each_pair do |coord, value|
119
+ @cell[coord] = sanitize_value(value) if value.is_a?(::String)
120
+ end
113
121
 
114
- @cell.each_pair do |coord, value|
115
- @cell[coord] = sanitize_value(value) if value.is_a?(::String)
122
+ @cleaned[sheet] = true
116
123
  end
117
124
 
118
- @cleaned[sheet] = true
125
+ alias_method :filename_or_stream, :filename
119
126
  end
120
127
  end
data/lib/roo/errors.rb ADDED
@@ -0,0 +1,11 @@
1
+ module Roo
2
+ # A base error class for Roo. Most errors thrown by Roo should inherit from
3
+ # this class.
4
+ class Error < StandardError; end
5
+
6
+ # Raised when Roo cannot find a header row that matches the given column
7
+ # name(s).
8
+ class HeaderRowNotFoundError < Error; end
9
+
10
+ class FileNotFound < Error; end
11
+ end
@@ -0,0 +1,94 @@
1
+ module Roo
2
+ class Excelx
3
+ class Cell
4
+ class Base
5
+ attr_reader :cell_type, :cell_value, :value
6
+
7
+ # FIXME: I think style should be deprecated. Having a style attribute
8
+ # for a cell doesn't really accomplish much. It seems to be used
9
+ # when you want to export to excelx.
10
+ attr_reader :style
11
+
12
+
13
+ # FIXME: Updating a cell's value should be able tochange the cell's type,
14
+ # but that isn't currently possible. This will cause weird bugs
15
+ # when one changes the value of a Number cell to a String. e.g.
16
+ #
17
+ # cell = Cell::Number(*args)
18
+ # cell.value = 'Hello'
19
+ # cell.formatted_value # => Some unexpected value
20
+ #
21
+ # Here are two possible solutions to such issues:
22
+ # 1. Don't allow a cell's value to be updated. Use a method like
23
+ # `Sheet.update_cell` instead. The simple solution.
24
+ # 2. When `cell.value = ` is called, use injection to try and
25
+ # change the type of cell on the fly. But deciding what type
26
+ # of value to pass to `cell.value=`. isn't always obvious. e.g.
27
+ # `cell.value = Time.now` should convert a cell to a DateTime,
28
+ # not a Time cell. Time cells would be hard to recognize because
29
+ # they are integers. This approach would require a significant
30
+ # change to the code as written. The complex solution.
31
+ #
32
+ # If the first solution is used, then this method should be
33
+ # deprecated.
34
+ attr_writer :value
35
+
36
+ def initialize(value, formula, excelx_type, style, link, coordinate)
37
+ @link = !!link
38
+ @cell_value = value
39
+ @cell_type = excelx_type
40
+ @formula = formula
41
+ @style = style
42
+ @coordinate = coordinate
43
+ @type = :base
44
+ @value = link? ? Roo::Link.new(link, value) : value
45
+ end
46
+
47
+ def type
48
+ if formula?
49
+ :formula
50
+ elsif link?
51
+ :link
52
+ else
53
+ @type
54
+ end
55
+ end
56
+
57
+ def formula?
58
+ !!@formula
59
+ end
60
+
61
+ def link?
62
+ !!@link
63
+ end
64
+
65
+ alias_method :formatted_value, :value
66
+
67
+ def to_s
68
+ formatted_value
69
+ end
70
+
71
+ # DEPRECATED: Please use link instead.
72
+ def hyperlink
73
+ warn '[DEPRECATION] `hyperlink` is deprecated. Please use `link` instead.'
74
+ end
75
+
76
+ # DEPRECATED: Please use cell_value instead.
77
+ def excelx_value
78
+ warn '[DEPRECATION] `excelx_value` is deprecated. Please use `cell_value` instead.'
79
+ cell_value
80
+ end
81
+
82
+ # DEPRECATED: Please use cell_type instead.
83
+ def excelx_type
84
+ warn '[DEPRECATION] `excelx_type` is deprecated. Please use `cell_type` instead.'
85
+ cell_type
86
+ end
87
+
88
+ def empty?
89
+ false
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,27 @@
1
+ module Roo
2
+ class Excelx
3
+ class Cell
4
+ class Boolean < Cell::Base
5
+ attr_reader :value, :formula, :format, :cell_type, :cell_value, :link, :coordinate
6
+
7
+ 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)
11
+ end
12
+
13
+ def formatted_value
14
+ value ? 'TRUE'.freeze : 'FALSE'.freeze
15
+ end
16
+
17
+ private
18
+
19
+ def create_boolean(value)
20
+ # FIXME: Using a boolean will cause methods like Base#to_csv to fail.
21
+ # Roo is using some method to ignore false/nil values.
22
+ value.to_i == 1 ? true : false
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,28 @@
1
+ require 'date'
2
+
3
+ module Roo
4
+ class Excelx
5
+ class Cell
6
+ class Date < Roo::Excelx::Cell::DateTime
7
+ attr_reader :value, :formula, :format, :cell_type, :cell_value, :link, :coordinate
8
+
9
+ def initialize(value, formula, excelx_type, style, link, base_date, coordinate)
10
+ # NOTE: Pass all arguments to the parent class, DateTime.
11
+ super
12
+ @type = :date
13
+ @format = excelx_type.last
14
+ @value = link? ? Roo::Link.new(link, value) : create_date(base_date, value)
15
+ end
16
+
17
+ private
18
+
19
+ def create_date(base_date, value)
20
+ date = base_date + value.to_i
21
+ yyyy, mm, dd = date.strftime('%Y-%m-%d').split('-')
22
+
23
+ ::Date.new(yyyy.to_i, mm.to_i, dd.to_i)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,111 @@
1
+ require 'date'
2
+
3
+ module Roo
4
+ class Excelx
5
+ class Cell
6
+ class DateTime < Cell::Base
7
+ attr_reader :value, :formula, :format, :cell_value, :link, :coordinate
8
+
9
+ def initialize(value, formula, excelx_type, style, link, base_date, coordinate)
10
+ super(value, formula, excelx_type, style, link, coordinate)
11
+ @type = :datetime
12
+ @format = excelx_type.last
13
+ @value = link? ? Roo::Link.new(link, value) : create_datetime(base_date, value)
14
+ end
15
+
16
+ # Public: Returns formatted value for a datetime. Format's can be an
17
+ # standard excel format, or a custom format.
18
+ #
19
+ # Standard formats follow certain conventions. Date fields for
20
+ # days, months, and years are separated with hyhens or
21
+ # slashes ("-", /") (e.g. 01-JAN, 1/13/15). Time fields for
22
+ # hours, minutes, and seconds are separated with a colon (e.g.
23
+ # 12:45:01).
24
+ #
25
+ # If a custom format follows those conventions, then the custom
26
+ # format will be used for the a cell's formatted value.
27
+ # Otherwise, the formatted value will be in the following
28
+ # format: 'YYYY-mm-dd HH:MM:SS' (e.g. "2015-07-10 20:33:15").
29
+ #
30
+ # Examples
31
+ # formatted_value #=> '01-JAN'
32
+ #
33
+ # Returns a String representation of a cell's value.
34
+ def formatted_value
35
+ formatter = @format.downcase.split(' ').map do |part|
36
+ if (parsed_format = parse_date_or_time_format(part))
37
+ parsed_format
38
+ else
39
+ warn 'Unable to parse custom format. Using "YYYY-mm-dd HH:MM:SS" format.'
40
+ return @value.strftime('%F %T')
41
+ end
42
+ end.join(' ')
43
+
44
+ @value.strftime(formatter)
45
+ end
46
+
47
+ private
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
+
64
+ DATE_FORMATS = {
65
+ 'yyyy' => '%Y', # Year: 2000
66
+ 'yy' => '%y', # Year: 00
67
+ # mmmmm => J-D
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
76
+ # '\\\\'.freeze => ''.freeze, # NOTE: Fixes a custom format's output.
77
+ }
78
+
79
+ TIME_FORMATS = {
80
+ 'hh' => '%H', # Hour (24): 01
81
+ 'h' => '%-k'.freeze, # Hour (24): 1
82
+ # 'hh'.freeze => '%I'.freeze, # Hour (12): 08
83
+ # 'h'.freeze => '%-l'.freeze, # Hour (12): 8
84
+ 'mm' => '%M', # Minute: 01
85
+ # FIXME: is this used? Seems like 'm' is used for month, not minute.
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.
93
+ }
94
+
95
+ def create_datetime(base_date, value)
96
+ date = base_date + value.to_f.round(6)
97
+ datetime_string = date.strftime('%Y-%m-%d %H:%M:%S.%N')
98
+ t = round_datetime(datetime_string)
99
+
100
+ ::DateTime.civil(t.year, t.month, t.day, t.hour, t.min, t.sec)
101
+ end
102
+
103
+ def round_datetime(datetime_string)
104
+ /(?<yyyy>\d+)-(?<mm>\d+)-(?<dd>\d+) (?<hh>\d+):(?<mi>\d+):(?<ss>\d+.\d+)/ =~ datetime_string
105
+
106
+ ::Time.new(yyyy, mm, dd, hh, mi, ss.to_r).round(0)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,19 @@
1
+
2
+ module Roo
3
+ class Excelx
4
+ class Cell
5
+ class Empty < Cell::Base
6
+ attr_reader :value, :formula, :format, :cell_type, :cell_value, :hyperlink, :coordinate
7
+
8
+ def initialize(coordinate)
9
+ @value = @formula = @format = @cell_type = @cell_value = @hyperlink = nil
10
+ @coordinate = coordinate
11
+ end
12
+
13
+ def empty?
14
+ true
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,87 @@
1
+ module Roo
2
+ class Excelx
3
+ class Cell
4
+ class Number < Cell::Base
5
+ attr_reader :value, :formula, :format, :cell_value, :link, :coordinate
6
+
7
+ def initialize(value, formula, excelx_type, style, link, coordinate)
8
+ super
9
+ # FIXME: change @type to number. This will break brittle tests.
10
+ # FIXME: Excelx_type is an array, but the first value isn't used.
11
+ @type = :float
12
+ @format = excelx_type.last
13
+ @value = link? ? Roo::Link.new(link, value) : create_numeric(value)
14
+ end
15
+
16
+ def create_numeric(number)
17
+ return number if Excelx::ERROR_VALUES.include?(number)
18
+ case @format
19
+ when /%/
20
+ Float(number)
21
+ when /\.0/
22
+ Float(number)
23
+ else
24
+ (number.include?('.') || (/\A[-+]?\d+E[-+]\d+\z/i =~ number)) ? Float(number) : Integer(number)
25
+ end
26
+ end
27
+
28
+ def formatted_value
29
+ return @cell_value if Excelx::ERROR_VALUES.include?(@cell_value)
30
+
31
+ formatter = formats[@format]
32
+ if formatter.is_a? Proc
33
+ formatter.call(@cell_value)
34
+ elsif zero_padded_number?
35
+ "%0#{@format.size}d" % @cell_value
36
+ else
37
+ Kernel.format(formatter, @cell_value)
38
+ end
39
+ end
40
+
41
+ def formats
42
+ # FIXME: numbers can be other colors besides red:
43
+ # [BLACK], [BLUE], [CYAN], [GREEN], [MAGENTA], [RED], [WHITE], [YELLOW], [COLOR n]
44
+ {
45
+ 'General' => '%.0f',
46
+ '0' => '%.0f',
47
+ '0.00' => '%.2f',
48
+ '0.000000' => '%.6f',
49
+ '#,##0' => number_format('%.0f'),
50
+ '#,##0.00' => number_format('%.2f'),
51
+ '0%' => proc do |number|
52
+ Kernel.format('%d%', number.to_f * 100)
53
+ end,
54
+ '0.00%' => proc do |number|
55
+ Kernel.format('%.2f%', number.to_f * 100)
56
+ end,
57
+ '0.00E+00' => '%.2E',
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)'),
62
+ # FIXME: not quite sure what the format should look like in this case.
63
+ '##0.0E+0' => '%.1E',
64
+ '@' => proc { |number| number }
65
+ }
66
+ end
67
+
68
+ private
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
+
81
+ def zero_padded_number?
82
+ @format[/0+/] == @format
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end
@@ -0,0 +1,19 @@
1
+ module Roo
2
+ class Excelx
3
+ class Cell
4
+ class String < Cell::Base
5
+ attr_reader :value, :formula, :format, :cell_type, :cell_value, :link, :coordinate
6
+
7
+ def initialize(value, formula, style, link, coordinate)
8
+ super(value, formula, nil, style, link, coordinate)
9
+ @type = @cell_type = :string
10
+ @value = link? ? Roo::Link.new(link, value) : value
11
+ end
12
+
13
+ def empty?
14
+ value.empty?
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,43 @@
1
+ require 'date'
2
+
3
+ module Roo
4
+ class Excelx
5
+ class Cell
6
+ class Time < Roo::Excelx::Cell::DateTime
7
+ attr_reader :value, :formula, :format, :cell_value, :link, :coordinate
8
+
9
+ def initialize(value, formula, excelx_type, style, link, base_date, coordinate)
10
+ # NOTE: Pass all arguments to DateTime super class.
11
+ super
12
+ @type = :time
13
+ @format = excelx_type.last
14
+ @datetime = create_datetime(base_date, value)
15
+ @value = link? ? Roo::Link.new(link, value) : (value.to_f * 86_400).to_i
16
+ end
17
+
18
+ def formatted_value
19
+ formatter = @format.gsub(/#{TIME_FORMATS.keys.join('|')}/, TIME_FORMATS)
20
+ @datetime.strftime(formatter)
21
+ end
22
+
23
+ alias_method :to_s, :formatted_value
24
+
25
+ private
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
40
+ end
41
+ end
42
+ end
43
+ end