aboutsummaryrefslogtreecommitdiffstats
path: root/lib/roo/excelx/cell/datetime.rb
blob: 63f3260b40f817cc4bf2ccb2f8160c773f144f13 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# frozen_string_literal: true

require 'date'

module Roo
  class Excelx
    class Cell
      class DateTime < Cell::Base
        SECONDS_IN_DAY = 60 * 60 * 24

        attr_reader :value, :formula, :format, :cell_value, :coordinate

        attr_reader_with_default default_type: :datetime

        def initialize(value, formula, excelx_type, style, link, base_timestamp, coordinate)
          super(value, formula, excelx_type, style, nil, coordinate)
          @format = excelx_type.last
          @value = link ? Roo::Link.new(link, value) : create_datetime(base_timestamp, value)
        end

        # Public: Returns formatted value for a datetime. Format's can be an
        #         standard excel format, or a custom format.
        #
        #         Standard formats follow certain conventions. Date fields for
        #         days, months, and years are separated with hyhens or
        #         slashes ("-", /") (e.g. 01-JAN, 1/13/15). Time fields for
        #         hours, minutes, and seconds are separated with a colon (e.g.
        #         12:45:01).
        #
        #         If a custom format follows those conventions, then the custom
        #         format will be used for the a cell's formatted value.
        #         Otherwise, the formatted value will be in the following
        #         format: 'YYYY-mm-dd HH:MM:SS' (e.g. "2015-07-10 20:33:15").
        #
        # Examples
        #    formatted_value #=> '01-JAN'
        #
        # Returns a String representation of a cell's value.
        def formatted_value
          formatter = @format.downcase.split(' ').map do |part|
            if (parsed_format = parse_date_or_time_format(part))
              parsed_format
            else
              warn 'Unable to parse custom format. Using "YYYY-mm-dd HH:MM:SS" format.'
              return @value.strftime('%F %T')
            end
          end.join(' ')

          @value.strftime(formatter)
        end

        private

        def parse_date_or_time_format(part)
          date_regex = /(?<date>[dmy]+[\-\/][dmy]+([\-\/][dmy]+)?)/
          time_regex = /(?<time>(\[?[h]\]?+:)?[m]+(:?ss|:?s)?)/

          if part[date_regex] == part
            formats = DATE_FORMATS
          elsif part[time_regex]
            formats = TIME_FORMATS
          else
            return false
          end

          part.gsub(/#{formats.keys.join('|')}/, formats)
        end

        DATE_FORMATS = {
          'yyyy' => '%Y',  # Year: 2000
          'yy' => '%y',    # Year: 00
          # mmmmm => J-D
          'mmmm' => '%B',  # Month: January
          'mmm' => '%^b',   # Month: JAN
          'mm' => '%m',    # Month: 01
          'm' => '%-m',    # Month: 1
          'dddd' => '%A',  # Day of the Week: Sunday
          'ddd' => '%^a',   # Day of the Week: SUN
          'dd' => '%d',    # Day of the Month: 01
          'd' => '%-d'    # Day of the Month: 1
          # '\\\\'.freeze => ''.freeze,  # NOTE: Fixes a custom format's output.
        }

        TIME_FORMATS = {
          'hh' => '%H',    # Hour (24): 01
          'h' => '%-k',    # Hour (24): 1
          # 'hh'.freeze => '%I'.freeze,    # Hour (12): 08
          # 'h'.freeze => '%-l'.freeze,    # Hour (12): 8
          'mm' => '%M',    # Minute: 01
          # FIXME: is this used? Seems like 'm' is used for month, not minute.
          'm' => '%-M',    # Minute: 1
          'ss' => '%S',    # Seconds: 01
          's' => '%-S',    # Seconds: 1
          'am/pm' => '%p', # Meridian: AM
          '000' => '%3N',  # Fractional Seconds: thousandth.
          '00' => '%2N',   # Fractional Seconds: hundredth.
          '0' => '%1N'    # Fractional Seconds: tenths.
        }

        def create_datetime(base_timestamp, value)
          timestamp = (base_timestamp + (value.to_f.round(6) * SECONDS_IN_DAY)).round(0)
          ::Time.at(timestamp).utc.to_datetime
        end
      end
    end
  end
end