File: validated_rule.rb

package info (click to toggle)
ruby-ice-cube 0.16.4-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 800 kB
  • sloc: ruby: 7,823; makefile: 6
file content (189 lines) | stat: -rw-r--r-- 4,877 bytes parent folder | download | duplicates (2)
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
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
require 'ice_cube/input_alignment'

module IceCube

  class ValidatedRule < Rule

    include Validations::ScheduleLock

    include Validations::Count
    include Validations::Until

    # Validations ordered for efficiency in sequence of:
    # * descending intervals
    # * boundary limits
    # * base values by cardinality (n = 60, 60, 31, 24, 12, 7)
    # * locks by cardinality (n = 365, 60, 60, 31, 24, 12, 7)
    # * interval multiplier
    VALIDATION_ORDER = [
      :year, :month, :day, :wday, :hour, :min, :sec, :count, :until,
      :base_sec, :base_min, :base_day, :base_hour, :base_month, :base_wday,
      :day_of_year, :second_of_minute, :minute_of_hour, :day_of_month,
      :hour_of_day, :month_of_year, :day_of_week,
      :interval
    ]

    attr_reader :validations

    def initialize(interval = 1)
      @validations = Hash.new
    end

    # Reset the uses on the rule to 0
    def reset
      @time = nil
      @start_time = nil
      @uses = 0
    end

    def base_interval_validation
      @validations[:interval].first
    end

    def other_interval_validations
      Array(@validations[base_interval_validation.type])
    end

    # Compute the next time after (or including) the specified time in respect
    # to the given start time
    def next_time(time, start_time, closing_time)
      @time = time
      unless @start_time
        @start_time = realign(time, start_time)
        @time = @start_time if @time < @start_time
      end

      return nil unless find_acceptable_time_before(closing_time)

      @uses += 1 if @time
      @time
    end

    def realign(opening_time, start_time)
      start_time
    end

    def full_required?
      !occurrence_count.nil?
    end

    def to_s
      builder = StringBuilder.new
      @validations.each_value do |validations|
        validations.each do |validation|
          validation.build_s(builder)
        end
      end
      builder.to_s
    end

    def to_hash
      builder = HashBuilder.new(self)
      @validations.each_value do |validations|
        validations.each do |validation|
          validation.build_hash(builder)
        end
      end
      builder.to_hash
    end

    def to_ical
      builder = IcalBuilder.new
      @validations.each_value do |validations|
        validations.each do |validation|
          validation.build_ical(builder)
        end
      end
      builder.to_s
    end

    # Get the collection that contains validations of a certain type
    def validations_for(key)
      @validations[key] ||= []
    end

    # Fully replace validations
    def replace_validations_for(key, arr)
      if arr.nil?
        @validations.delete(key)
      else
        @validations[key] = arr
      end
    end

    # Remove the specified base validations
    def clobber_base_validations(*types)
      types.each do |type|
        @validations.delete(:"base_#{type}")
      end
    end

    private

    def normalized_interval(interval)
      int = interval.to_i
      raise ArgumentError, "'#{interval}' is not a valid input for interval. Please pass a postive integer." unless int > 0
      int
    end

    def finds_acceptable_time?
      validation_names.all? do |type|
        validation_accepts_or_updates_time?(@validations[type])
      end
    end

    def find_acceptable_time_before(boundary)
      until finds_acceptable_time?
        return false if past_closing_time?(boundary)
      end
      true
    end

    # Returns true if all validations for the current rule match
    # otherwise false and shifts to the first (largest) unmatched offset
    #
    def validation_accepts_or_updates_time?(validations_for_type)
      res = validations_for_type.each_with_object([]) do |validation, offsets|
        r = validation.validate(@time, @start_time)
        return true if r.nil? || r == 0
        offsets << r
      end
      shift_time_by_validation(res, validations_for_type.first)
      false
    end

    def shift_time_by_validation(res, validation)
      return unless (interval = res.min)
      wrapper = TimeUtil::TimeWrapper.new(@time, validation.dst_adjust?)
      wrapper.add(validation.type, interval)
      wrapper.clear_below(validation.type)

      # Move over DST if blocked, no adjustments
      if wrapper.to_time <= @time
        wrapper = TimeUtil::TimeWrapper.new(wrapper.to_time, false)
        until wrapper.to_time > @time
          wrapper.add(:min, 10) # smallest interval
        end
      end

      # And then get the correct time out
      @time = wrapper.to_time
    end

    def past_closing_time?(closing_time)
      closing_time && @time > closing_time
    end

    def validation_names
      VALIDATION_ORDER & @validations.keys
    end

    def verify_alignment(value, freq, rule_part)
      InputAlignment.new(self, value, rule_part).verify(freq) do |error|
        yield error
      end
    end

  end

end