File: parser.rb

package info (click to toggle)
ruby-timeliness 0.3.10-2
  • links: PTS, VCS
  • area: main
  • in suites: bookworm, bullseye
  • size: 216 kB
  • sloc: ruby: 1,456; makefile: 4
file content (168 lines) | stat: -rw-r--r-- 5,162 bytes parent folder | download
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
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
module Timeliness
  module Parser
    class MissingTimezoneSupport < StandardError; end

    class << self

      def parse(value, *args)
        return value if acts_like_temporal?(value)
        return nil unless parseable?(value)

        type, options = type_and_options_from_args(args)

        time_array = _parse(value, type, options)
        return nil if time_array.nil?

        default_values_by_type(time_array, type, options) unless type == :datetime

        make_time(time_array[0..7], options[:zone])
      rescue NoMethodError => ex
        raise ex unless ex.message =~ /undefined method `(zone|use_zone|current)' for Time:Class/
        raise MissingTimezoneSupport, "ActiveSupport timezone support must be loaded to use timezones other than :utc and :local."
      end

      def make_time(time_array, zone_option=nil)
        return nil unless fast_date_valid_with_fallback(*time_array[0..2])

        zone, offset = zone_and_offset(time_array[7]) if time_array[7]

        value = create_time_in_zone(time_array[0..6].compact, zone || zone_option)
        value = shift_time_to_zone(value, zone_option) if zone
        return nil unless value

        offset ? value + (value.utc_offset - offset) : value
      rescue ArgumentError, TypeError
        nil
      end

      def _parse(string, type=nil, options={})
        if options[:strict] && type
          Definitions.send("#{type}_format_set").match(string, options[:format])
        else
          values = nil
          Definitions.format_sets(type, string).find {|set| values = set.match(string, options[:format]) }
          values
        end
      rescue
        nil
      end

      private

      def parseable?(value)
        value.is_a?(String)
      end

      def acts_like_temporal?(value)
        value.is_a?(Time) || value.is_a?(Date) || value.respond_to?(:acts_like_date?) || value.respond_to?(:acts_like_time?)
      end

      def type_and_options_from_args(args)
        options = args.last.is_a?(Hash) ? args.pop : {}
        type_or_now = args.first
        if type_or_now.is_a?(Symbol)
          type = type_or_now
        elsif type_or_now
          options[:now] = type_or_now
        end
        return type, options
      end

      def default_values_by_type(values, type, options)
        case type
        when :date
          values[3..7] = nil
        when :time
          values[0..2] = current_date(options)
        when nil
          dummy_date = current_date(options)
          values[0] ||= dummy_date[0]
          values[1] ||= dummy_date[1] unless values.values_at(0,2).all?
          values[2] ||= dummy_date[2]
        end
      end

      def current_date(options)
        now = if options[:now]
          options[:now]
        elsif options[:zone]
          current_time_in_zone(options[:zone])
        else
          evaluate_date_for_time_type
        end
        now.is_a?(Array) ? now[0..2] : [now.year, now.month, now.day]
      end

      def current_time_in_zone(zone)
        case zone
        when :utc, :local
          Time.now.send("get#{zone}")
        when :current
          Time.current
        else
          Time.use_zone(zone) { Time.current }
        end
      end

      def shift_time_to_zone(time, zone=nil)
        zone ||= Timeliness.default_timezone
        case zone
        when :utc, :local
          time.send("get#{zone}")
        when :current
          time.in_time_zone
        else
          Time.use_zone(zone) { time.in_time_zone }
        end
      end

      def create_time_in_zone(time_array, zone=nil)
        zone ||= Timeliness.default_timezone
        case zone
        when :utc, :local
          time_with_datetime_fallback(zone, *time_array)
        when :current
          Time.zone.local(*time_array)
        else
          Time.use_zone(zone) { Time.zone.local(*time_array) }
        end
      end

      def zone_and_offset(parsed_value)
        if parsed_value.is_a?(String)
          zone   = Definitions.timezone_mapping[parsed_value] || parsed_value
        else
          offset = parsed_value
        end
        return zone, offset
      end

      # Taken from ActiveSupport and simplified
      def time_with_datetime_fallback(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0, usec=0)
        return nil if hour > 23 || min > 59 || sec > 59
        ::Time.send(utc_or_local, year, month, day, hour, min, sec, usec)
      rescue
        offset = utc_or_local == :local ? (::Time.local(2007).utc_offset.to_r/86400) : 0
        ::DateTime.civil(year, month, day, hour, min, sec, offset)
      end

      # Enforce strict date part validity which the Time class does not.
      # Only does full date check if month and day are possibly invalid.
      def fast_date_valid_with_fallback(year, month, day)
        month && month < 13 && (day < 29 || Date.valid_civil?(year, month, day))
      end

      def evaluate_date_for_time_type
        case Timeliness.date_for_time_type
        when Array
          Timeliness.date_for_time_type
        when Proc
          v = Timeliness.date_for_time_type.call
          [v.year, v.month, v.day]
        end
      end

    end

  end
end