ruh-roo 3.0.1

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 (49) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +677 -0
  3. data/Gemfile +24 -0
  4. data/LICENSE +24 -0
  5. data/README.md +315 -0
  6. data/lib/roo/base.rb +607 -0
  7. data/lib/roo/constants.rb +7 -0
  8. data/lib/roo/csv.rb +141 -0
  9. data/lib/roo/errors.rb +11 -0
  10. data/lib/roo/excelx/cell/base.rb +108 -0
  11. data/lib/roo/excelx/cell/boolean.rb +30 -0
  12. data/lib/roo/excelx/cell/date.rb +28 -0
  13. data/lib/roo/excelx/cell/datetime.rb +107 -0
  14. data/lib/roo/excelx/cell/empty.rb +20 -0
  15. data/lib/roo/excelx/cell/number.rb +89 -0
  16. data/lib/roo/excelx/cell/string.rb +19 -0
  17. data/lib/roo/excelx/cell/time.rb +44 -0
  18. data/lib/roo/excelx/cell.rb +110 -0
  19. data/lib/roo/excelx/comments.rb +55 -0
  20. data/lib/roo/excelx/coordinate.rb +19 -0
  21. data/lib/roo/excelx/extractor.rb +39 -0
  22. data/lib/roo/excelx/format.rb +71 -0
  23. data/lib/roo/excelx/images.rb +26 -0
  24. data/lib/roo/excelx/relationships.rb +33 -0
  25. data/lib/roo/excelx/shared.rb +39 -0
  26. data/lib/roo/excelx/shared_strings.rb +151 -0
  27. data/lib/roo/excelx/sheet.rb +151 -0
  28. data/lib/roo/excelx/sheet_doc.rb +248 -0
  29. data/lib/roo/excelx/styles.rb +64 -0
  30. data/lib/roo/excelx/workbook.rb +63 -0
  31. data/lib/roo/excelx.rb +480 -0
  32. data/lib/roo/font.rb +17 -0
  33. data/lib/roo/formatters/base.rb +15 -0
  34. data/lib/roo/formatters/csv.rb +84 -0
  35. data/lib/roo/formatters/matrix.rb +23 -0
  36. data/lib/roo/formatters/xml.rb +31 -0
  37. data/lib/roo/formatters/yaml.rb +40 -0
  38. data/lib/roo/helpers/default_attr_reader.rb +20 -0
  39. data/lib/roo/helpers/weak_instance_cache.rb +41 -0
  40. data/lib/roo/libre_office.rb +4 -0
  41. data/lib/roo/link.rb +34 -0
  42. data/lib/roo/open_office.rb +628 -0
  43. data/lib/roo/spreadsheet.rb +39 -0
  44. data/lib/roo/tempdir.rb +21 -0
  45. data/lib/roo/utils.rb +128 -0
  46. data/lib/roo/version.rb +3 -0
  47. data/lib/roo.rb +36 -0
  48. data/roo.gemspec +28 -0
  49. metadata +189 -0
data/lib/roo/csv.rb ADDED
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "csv"
4
+ require "time"
5
+
6
+ # The CSV class can read csv files (must be separated with commas) which then
7
+ # can be handled like spreadsheets. This means you can access cells like A5
8
+ # within these files.
9
+ # The CSV class provides only string objects. If you want conversions to other
10
+ # types you have to do it yourself.
11
+ #
12
+ # You can pass options to the underlying CSV parse operation, via the
13
+ # :csv_options option.
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
23
+
24
+ def cell(row, col, sheet = nil)
25
+ sheet ||= default_sheet
26
+ read_cells(sheet)
27
+ @cell[normalize(row, col)]
28
+ end
29
+
30
+ def celltype(row, col, sheet = nil)
31
+ sheet ||= default_sheet
32
+ read_cells(sheet)
33
+ @cell_type[normalize(row, col)]
34
+ end
35
+
36
+ def cell_postprocessing(_row, _col, value)
37
+ value
38
+ end
39
+
40
+ def csv_options
41
+ @options[:csv_options] || {}
42
+ end
43
+
44
+ def set_value(row, col, value, _sheet)
45
+ @cell[[row, col]] = value
46
+ end
47
+
48
+ def set_type(row, col, type, _sheet)
49
+ @cell_type[[row, col]] = type
50
+ end
51
+
52
+ private
53
+
54
+ TYPE_MAP = {
55
+ String => :string,
56
+ Float => :float,
57
+ Date => :date,
58
+ DateTime => :datetime,
59
+ }
60
+
61
+ def celltype_class(value)
62
+ TYPE_MAP[value.class]
63
+ end
64
+
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
81
+
82
+ max_col_num = col_num if col_num > max_col_num
83
+ end
84
+
85
+ set_row_count(sheet, row_num)
86
+ set_column_count(sheet, max_col_num)
87
+ @cells_read[sheet] = true
88
+ end
89
+
90
+ def each_row(options, &block)
91
+ if uri?(filename)
92
+ each_row_using_tempdir(options, &block)
93
+ else
94
+ csv_foreach(filename_or_stream, options, &block)
95
+ end
96
+ end
97
+
98
+ def each_row_using_tempdir(options, &block)
99
+ ::Dir.mktmpdir(Roo::TEMP_PREFIX, ENV["ROO_TMP"]) do |tmpdir|
100
+ tmp_filename = download_uri(filename, tmpdir)
101
+ csv_foreach(tmp_filename, options, &block)
102
+ end
103
+ end
104
+
105
+ def csv_foreach(path_or_io, options, &block)
106
+ if is_stream?(path_or_io)
107
+ ::CSV.new(path_or_io, **options).each(&block)
108
+ else
109
+ ::CSV.foreach(path_or_io, **options, &block)
110
+ end
111
+ end
112
+
113
+ def set_row_count(sheet, last_row)
114
+ @first_row[sheet] = 1
115
+ @last_row[sheet] = last_row
116
+ @last_row[sheet] = @first_row[sheet] if @last_row[sheet].zero?
117
+
118
+ nil
119
+ end
120
+
121
+ def set_column_count(sheet, last_col)
122
+ @first_column[sheet] = 1
123
+ @last_column[sheet] = last_col
124
+ @last_column[sheet] = @first_column[sheet] if @last_column[sheet].zero?
125
+
126
+ nil
127
+ end
128
+
129
+ def clean_sheet(sheet)
130
+ read_cells(sheet)
131
+
132
+ @cell.each_pair do |coord, value|
133
+ @cell[coord] = sanitize_value(value) if value.is_a?(::String)
134
+ end
135
+
136
+ @cleaned[sheet] = true
137
+ end
138
+
139
+ alias_method :filename_or_stream, :filename
140
+ end
141
+ 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,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "roo/helpers/default_attr_reader"
4
+
5
+ module Roo
6
+ class Excelx
7
+ class Cell
8
+ class Base
9
+ extend Roo::Helpers::DefaultAttrReader
10
+ attr_reader :cell_type, :cell_value, :value
11
+
12
+ # FIXME: I think style should be deprecated. Having a style attribute
13
+ # for a cell doesn't really accomplish much. It seems to be used
14
+ # when you want to export to excelx.
15
+ attr_reader_with_default default_type: :base, style: 1
16
+
17
+
18
+ # FIXME: Updating a cell's value should be able tochange the cell's type,
19
+ # but that isn't currently possible. This will cause weird bugs
20
+ # when one changes the value of a Number cell to a String. e.g.
21
+ #
22
+ # cell = Cell::Number(*args)
23
+ # cell.value = 'Hello'
24
+ # cell.formatted_value # => Some unexpected value
25
+ #
26
+ # Here are two possible solutions to such issues:
27
+ # 1. Don't allow a cell's value to be updated. Use a method like
28
+ # `Sheet.update_cell` instead. The simple solution.
29
+ # 2. When `cell.value = ` is called, use injection to try and
30
+ # change the type of cell on the fly. But deciding what type
31
+ # of value to pass to `cell.value=`. isn't always obvious. e.g.
32
+ # `cell.value = Time.now` should convert a cell to a DateTime,
33
+ # not a Time cell. Time cells would be hard to recognize because
34
+ # they are integers. This approach would require a significant
35
+ # change to the code as written. The complex solution.
36
+ #
37
+ # If the first solution is used, then this method should be
38
+ # deprecated.
39
+ attr_writer :value
40
+
41
+ def initialize(value, formula, excelx_type, style, link, coordinate)
42
+ @cell_value = value
43
+ @cell_type = excelx_type if excelx_type
44
+ @formula = formula if formula
45
+ @style = style unless style == 1
46
+ @coordinate = coordinate
47
+ @value = link ? Roo::Link.new(link, value) : value
48
+ end
49
+
50
+ def type
51
+ if formula?
52
+ :formula
53
+ elsif link?
54
+ :link
55
+ else
56
+ default_type
57
+ end
58
+ end
59
+
60
+ def formula?
61
+ !!(defined?(@formula) && @formula)
62
+ end
63
+
64
+ def link?
65
+ Roo::Link === @value
66
+ end
67
+
68
+ alias_method :formatted_value, :value
69
+
70
+ def to_s
71
+ formatted_value
72
+ end
73
+
74
+ # DEPRECATED: Please use link? instead.
75
+ def hyperlink
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?
84
+ end
85
+
86
+ # DEPRECATED: Please use cell_value instead.
87
+ def excelx_value
88
+ warn '[DEPRECATION] `excelx_value` is deprecated. Please use `cell_value` instead.'
89
+ cell_value
90
+ end
91
+
92
+ # DEPRECATED: Please use cell_type instead.
93
+ def excelx_type
94
+ warn '[DEPRECATION] `excelx_type` is deprecated. Please use `cell_type` instead.'
95
+ cell_type
96
+ end
97
+
98
+ def empty?
99
+ false
100
+ end
101
+
102
+ def presence
103
+ empty? ? nil : self
104
+ end
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ class Excelx
5
+ class Cell
6
+ class Boolean < Cell::Base
7
+ attr_reader :value, :formula, :format, :cell_value, :coordinate
8
+
9
+ attr_reader_with_default default_type: :boolean, cell_type: :boolean
10
+
11
+ def initialize(value, formula, style, link, coordinate)
12
+ super(value, formula, nil, style, nil, coordinate)
13
+ @value = link ? Roo::Link.new(link, value) : create_boolean(value)
14
+ end
15
+
16
+ def formatted_value
17
+ value ? 'TRUE' : 'FALSE'
18
+ end
19
+
20
+ private
21
+
22
+ def create_boolean(value)
23
+ # FIXME: Using a boolean will cause methods like Base#to_csv to fail.
24
+ # Roo is using some method to ignore false/nil values.
25
+ value.to_i == 1
26
+ end
27
+ end
28
+ end
29
+ end
30
+ 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, :coordinate
8
+
9
+ attr_reader_with_default default_type: :date
10
+
11
+ def initialize(value, formula, excelx_type, style, link, base_date, coordinate)
12
+ # NOTE: Pass all arguments to the parent class, DateTime.
13
+ super
14
+ @format = excelx_type.last
15
+ @value = link ? Roo::Link.new(link, value) : create_date(base_date, value)
16
+ end
17
+
18
+ private
19
+
20
+ def create_datetime(_,_); end
21
+
22
+ def create_date(base_date, value)
23
+ base_date + value.to_i
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Roo
6
+ class Excelx
7
+ class Cell
8
+ class DateTime < Cell::Base
9
+ SECONDS_IN_DAY = 60 * 60 * 24
10
+
11
+ attr_reader :value, :formula, :format, :cell_value, :coordinate
12
+
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)
17
+ @format = excelx_type.last
18
+ @value = link ? Roo::Link.new(link, value) : create_datetime(base_timestamp, value)
19
+ end
20
+
21
+ # Public: Returns formatted value for a datetime. Format's can be an
22
+ # standard excel format, or a custom format.
23
+ #
24
+ # Standard formats follow certain conventions. Date fields for
25
+ # days, months, and years are separated with hyhens or
26
+ # slashes ("-", /") (e.g. 01-JAN, 1/13/15). Time fields for
27
+ # hours, minutes, and seconds are separated with a colon (e.g.
28
+ # 12:45:01).
29
+ #
30
+ # If a custom format follows those conventions, then the custom
31
+ # format will be used for the a cell's formatted value.
32
+ # Otherwise, the formatted value will be in the following
33
+ # format: 'YYYY-mm-dd HH:MM:SS' (e.g. "2015-07-10 20:33:15").
34
+ #
35
+ # Examples
36
+ # formatted_value #=> '01-JAN'
37
+ #
38
+ # Returns a String representation of a cell's value.
39
+ def formatted_value
40
+ formatter = @format.downcase.split(' ').map do |part|
41
+ if (parsed_format = parse_date_or_time_format(part))
42
+ parsed_format
43
+ else
44
+ warn 'Unable to parse custom format. Using "YYYY-mm-dd HH:MM:SS" format.'
45
+ return @value.strftime('%F %T')
46
+ end
47
+ end.join(' ')
48
+
49
+ @value.strftime(formatter)
50
+ end
51
+
52
+ private
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
+
69
+ DATE_FORMATS = {
70
+ 'yyyy' => '%Y', # Year: 2000
71
+ 'yy' => '%y', # Year: 00
72
+ # mmmmm => J-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
81
+ # '\\\\'.freeze => ''.freeze, # NOTE: Fixes a custom format's output.
82
+ }
83
+
84
+ TIME_FORMATS = {
85
+ 'hh' => '%H', # Hour (24): 01
86
+ 'h' => '%-k', # Hour (24): 1
87
+ # 'hh'.freeze => '%I'.freeze, # Hour (12): 08
88
+ # 'h'.freeze => '%-l'.freeze, # Hour (12): 8
89
+ 'mm' => '%M', # Minute: 01
90
+ # FIXME: is this used? Seems like 'm' is used for month, not minute.
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.
98
+ }
99
+
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
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,20 @@
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, :coordinate
7
+
8
+ attr_reader_with_default default_type: nil, style: nil
9
+
10
+ def initialize(coordinate)
11
+ @coordinate = coordinate
12
+ end
13
+
14
+ def empty?
15
+ true
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Roo
4
+ class Excelx
5
+ class Cell
6
+ class Number < Cell::Base
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
11
+
12
+ def initialize(value, formula, excelx_type, style, link, coordinate)
13
+ super
14
+ # FIXME: Excelx_type is an array, but the first value isn't used.
15
+ @format = excelx_type.last
16
+ @value = link ? Roo::Link.new(link, value) : create_numeric(value)
17
+ end
18
+
19
+ def create_numeric(number)
20
+ return number if Excelx::ERROR_VALUES.include?(number)
21
+ case @format
22
+ when /%/
23
+ Float(number)
24
+ when /\.0/
25
+ Float(number)
26
+ else
27
+ (number.include?('.') || (/\A[-+]?\d+E[-+]?\d+\z/i =~ number)) ? Float(number) : Integer(number, 10)
28
+ end
29
+ end
30
+
31
+ def formatted_value
32
+ return @cell_value if Excelx::ERROR_VALUES.include?(@cell_value)
33
+
34
+ formatter = generate_formatter(@format)
35
+ if formatter.is_a? Proc
36
+ formatter.call(@cell_value)
37
+ else
38
+ Kernel.format(formatter, @cell_value)
39
+ end
40
+ end
41
+
42
+ def generate_formatter(format)
43
+ # FIXME: numbers can be other colors besides red:
44
+ # [BLACK], [BLUE], [CYAN], [GREEN], [MAGENTA], [RED], [WHITE], [YELLOW], [COLOR n]
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.(0+)$/ then number_format("%.#{$1.size}f")
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)')
65
+ # FIXME: not quite sure what the format should look like in this case.
66
+ when '##0.0E+0' then '%.1E'
67
+ when "_-* #,##0.00\\ _€_-;\\-* #,##0.00\\ _€_-;_-* \"-\"??\\ _€_-;_-@_-" then number_format('%.2f', '-%.2f')
68
+ when '@' then proc { |number| number }
69
+ else
70
+ raise "Unknown format: #{format.inspect}"
71
+ end
72
+ end
73
+
74
+ private
75
+
76
+ def number_format(formatter, negative_formatter = nil)
77
+ proc do |number|
78
+ if negative_formatter
79
+ formatter = number.to_i > 0 ? formatter : negative_formatter
80
+ number = number.to_f.abs
81
+ end
82
+
83
+ Kernel.format(formatter, number).reverse.gsub(/(\d{3})(?=\d)/, '\\1,').reverse
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
89
+ 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_value, :coordinate
6
+
7
+ attr_reader_with_default default_type: :string, cell_type: :string
8
+
9
+ def initialize(value, formula, style, link, coordinate)
10
+ super(value, formula, nil, style, link, coordinate)
11
+ end
12
+
13
+ def empty?
14
+ value.empty?
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,44 @@
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, :coordinate
8
+
9
+ attr_reader_with_default default_type: :time
10
+
11
+ def initialize(value, formula, excelx_type, style, link, base_date, coordinate)
12
+ # NOTE: Pass all arguments to DateTime super class.
13
+ super
14
+ @format = excelx_type.last
15
+ @datetime = create_datetime(base_date, value)
16
+ @value = link ? Roo::Link.new(link, value) : (value.to_f * 86_400).round.to_i
17
+ end
18
+
19
+ def formatted_value
20
+ formatter = @format.gsub(/#{TIME_FORMATS.keys.join('|')}/, TIME_FORMATS)
21
+ @datetime.strftime(formatter)
22
+ end
23
+
24
+ alias_method :to_s, :formatted_value
25
+
26
+ private
27
+
28
+ # def create_datetime(base_date, value)
29
+ # date = base_date + value.to_f.round(6)
30
+ # datetime_string = date.strftime('%Y-%m-%d %H:%M:%S.%N')
31
+ # t = round_datetime(datetime_string)
32
+ #
33
+ # ::DateTime.civil(t.year, t.month, t.day, t.hour, t.min, t.sec)
34
+ # end
35
+
36
+ # def round_datetime(datetime_string)
37
+ # /(?<yyyy>\d+)-(?<mm>\d+)-(?<dd>\d+) (?<hh>\d+):(?<mi>\d+):(?<ss>\d+.\d+)/ =~ datetime_string
38
+ #
39
+ # ::Time.new(yyyy.to_i, mm.to_i, dd.to_i, hh.to_i, mi.to_i, ss.to_r).round(0)
40
+ # end
41
+ end
42
+ end
43
+ end
44
+ end