timers 4.3.2 → 4.3.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 52ef1e26aab6a7bbbf5c9d687028bdc36f52810dd7ebc0c76873b6bef9f30ca2
4
- data.tar.gz: 26105fe04a6d2bfcdf9efb46ebd83d9562a08af3c2f113892a8be57713be6112
3
+ metadata.gz: a9f471bfb9cac57f9462498ab12527eded7533318cec13604a10d32b159c6689
4
+ data.tar.gz: 69802b0774101037596274f2acf114c3f4234875ed8a82146e8ef1075407bf6d
5
5
  SHA512:
6
- metadata.gz: 2a3ae0e0d9644627fef3aeffdfde86456d74c28ab89194486743fee2a25e8832c5aafb05f88174f6ee27d4f9402e05b4f80776c49cc34d70da527e63f9097685
7
- data.tar.gz: 1f30b97aa11f6f42b3ce1786ae42f06334a86064306601635774b8d84cc88df765e8312a455798984520d44e3db35f8094fb23ec35cb736eb2191bfa27dece0c
6
+ metadata.gz: f15934803658ec4b1d0d7b4a8ea92c9a21e2267544ec11c9619604288264c7f0add19dd12b71a0680827d6d37b853192d98266f29b06ef27cb36e53408f8c67b
7
+ data.tar.gz: a50a935d7b73681b4b751341c6eda0143c64beb9d469641ea68b8481cd1c36bd16ad21f7ec274f06d1389b53c5849e06b20182d3faea6497f64773d8fba2dae6
data/lib/timers/events.rb CHANGED
@@ -21,57 +21,52 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  require_relative "timer"
24
+ require_relative "priority_heap"
24
25
 
25
26
  module Timers
26
- # Maintains an ordered list of events, which can be cancelled.
27
+ # Maintains a PriorityHeap of events ordered on time, which can be cancelled.
27
28
  class Events
28
29
  # Represents a cancellable handle for a specific timer event.
29
30
  class Handle
31
+ include Comparable
32
+
30
33
  def initialize(time, callback)
31
34
  @time = time
32
35
  @callback = callback
33
36
  end
34
-
37
+
35
38
  # The absolute time that the handle should be fired at.
36
39
  attr_reader :time
37
-
40
+
38
41
  # Cancel this timer, O(1).
39
42
  def cancel!
40
43
  # The simplest way to keep track of cancelled status is to nullify the
41
44
  # callback. This should also be optimal for garbage collection.
42
45
  @callback = nil
43
46
  end
44
-
47
+
45
48
  # Has this timer been cancelled? Cancelled timer's don't fire.
46
49
  def cancelled?
47
50
  @callback.nil?
48
51
  end
49
-
50
- def > other
51
- @time > other.to_f
52
- end
53
-
54
- def >= other
55
- @time >= other.to_f
56
- end
57
-
58
- def to_f
59
- @time
52
+
53
+ def <=> other
54
+ @time <=> other.time
60
55
  end
61
-
56
+
62
57
  # Fire the callback if not cancelled with the given time parameter.
63
58
  def fire(time)
64
59
  @callback.call(time) if @callback
65
60
  end
66
61
  end
67
-
62
+
68
63
  def initialize
69
64
  # A sequence of handles, maintained in sorted order, future to present.
70
65
  # @sequence.last is the next event to be fired.
71
- @sequence = []
66
+ @sequence = PriorityHeap.new
72
67
  @queue = []
73
68
  end
74
-
69
+
75
70
  # Add an event at the given time.
76
71
  def schedule(time, callback)
77
72
  handle = Handle.new(time.to_f, callback)
@@ -80,64 +75,41 @@ module Timers
80
75
 
81
76
  return handle
82
77
  end
83
-
78
+
84
79
  # Returns the first non-cancelled handle.
85
80
  def first
86
81
  merge!
87
82
 
88
- while (handle = @sequence.last)
83
+ while (handle = @sequence.peek)
89
84
  return handle unless handle.cancelled?
90
85
  @sequence.pop
91
86
  end
92
87
  end
93
-
88
+
94
89
  # Returns the number of pending (possibly cancelled) events.
95
90
  def size
96
91
  @sequence.size + @queue.size
97
92
  end
98
-
93
+
99
94
  # Fire all handles for which Handle#time is less than the given time.
100
95
  def fire(time)
101
96
  merge!
102
97
 
103
- while handle = @sequence.last and handle.time <= time
98
+ while handle = @sequence.peek and handle.time <= time
104
99
  @sequence.pop
105
100
  handle.fire(time)
106
101
  end
107
102
  end
108
-
103
+
109
104
  private
110
-
105
+
106
+ # Move all non-cancelled timers from the pending queue to the priority heap
111
107
  def merge!
112
108
  while handle = @queue.pop
113
109
  next if handle.cancelled?
114
110
 
115
- index = bisect_right(@sequence, handle)
116
-
117
- if current_handle = @sequence[index] and current_handle.cancelled?
118
- # puts "Replacing handle at index: #{index} due to cancellation in array containing #{@sequence.size} item(s)."
119
- @sequence[index] = handle
120
- else
121
- # puts "Inserting handle at index: #{index} in array containing #{@sequence.size} item(s)."
122
- @sequence.insert(index, handle)
123
- end
111
+ @sequence.push(handle)
124
112
  end
125
113
  end
126
-
127
- # Return the right-most index where to insert item e, in a list a, assuming
128
- # a is sorted in descending order.
129
- def bisect_right(a, e, l = 0, u = a.length)
130
- while l < u
131
- m = l + (u - l).div(2)
132
-
133
- if a[m] >= e
134
- l = m + 1
135
- else
136
- u = m
137
- end
138
- end
139
-
140
- l
141
- end
142
114
  end
143
115
  end
data/lib/timers/group.rb CHANGED
@@ -31,62 +31,62 @@ module Timers
31
31
  # A collection of timers which may fire at different times
32
32
  class Group
33
33
  include Enumerable
34
-
34
+
35
35
  extend Forwardable
36
36
  def_delegators :@timers, :each, :empty?
37
-
37
+
38
38
  def initialize
39
39
  @events = Events.new
40
-
40
+
41
41
  @timers = Set.new
42
42
  @paused_timers = Set.new
43
-
43
+
44
44
  @interval = Interval.new
45
45
  @interval.start
46
46
  end
47
-
47
+
48
48
  # Scheduled events:
49
49
  attr_reader :events
50
-
50
+
51
51
  # Active timers:
52
52
  attr_reader :timers
53
-
53
+
54
54
  # Paused timers:
55
55
  attr_reader :paused_timers
56
-
56
+
57
57
  # Call the given block after the given interval. The first argument will be
58
58
  # the time at which the group was asked to fire timers for.
59
59
  def after(interval, &block)
60
60
  Timer.new(self, interval, false, &block)
61
61
  end
62
-
62
+
63
63
  # Call the given block immediately, and then after the given interval. The first
64
64
  # argument will be the time at which the group was asked to fire timers for.
65
65
  def now_and_after(interval, &block)
66
66
  yield
67
67
  after(interval, &block)
68
68
  end
69
-
69
+
70
70
  # Call the given block periodically at the given interval. The first
71
71
  # argument will be the time at which the group was asked to fire timers for.
72
72
  def every(interval, recur = true, &block)
73
73
  Timer.new(self, interval, recur, &block)
74
74
  end
75
-
75
+
76
76
  # Call the given block immediately, and then periodically at the given interval. The first
77
77
  # argument will be the time at which the group was asked to fire timers for.
78
78
  def now_and_every(interval, recur = true, &block)
79
79
  yield
80
80
  every(interval, recur, &block)
81
81
  end
82
-
82
+
83
83
  # Wait for the next timer and fire it. Can take a block, which should behave
84
84
  # like sleep(n), except that n may be nil (sleep forever) or a negative
85
85
  # number (fire immediately after return).
86
86
  def wait
87
87
  if block_given?
88
88
  yield wait_interval
89
-
89
+
90
90
  while (interval = wait_interval) && interval > 0
91
91
  yield interval
92
92
  end
@@ -99,7 +99,7 @@ module Timers
99
99
 
100
100
  fire
101
101
  end
102
-
102
+
103
103
  # Interval to wait until when the next timer will fire.
104
104
  # - nil: no timers
105
105
  # - -ve: timers expired already
@@ -109,36 +109,36 @@ module Timers
109
109
  handle = @events.first
110
110
  handle.time - Float(offset) if handle
111
111
  end
112
-
112
+
113
113
  # Fire all timers that are ready.
114
114
  def fire(offset = current_offset)
115
115
  @events.fire(offset)
116
116
  end
117
-
117
+
118
118
  # Pause all timers.
119
119
  def pause
120
120
  @timers.dup.each(&:pause)
121
121
  end
122
-
122
+
123
123
  # Resume all timers.
124
124
  def resume
125
125
  @paused_timers.dup.each(&:resume)
126
126
  end
127
-
127
+
128
128
  alias continue resume
129
-
129
+
130
130
  # Delay all timers.
131
131
  def delay(seconds)
132
132
  @timers.each do |timer|
133
133
  timer.delay(seconds)
134
134
  end
135
135
  end
136
-
136
+
137
137
  # Cancel all timers.
138
138
  def cancel
139
139
  @timers.dup.each(&:cancel)
140
140
  end
141
-
141
+
142
142
  # The group's current time.
143
143
  def current_offset
144
144
  @interval.to_f
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Copyright, 2021, by Wander Hillen.
4
+ # Copyright, 2021, by Samuel G. D. Williams. <http://www.codeotaku.com>
5
+ #
6
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ # of this software and associated documentation files (the "Software"), to deal
8
+ # in the Software without restriction, including without limitation the rights
9
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ # copies of the Software, and to permit persons to whom the Software is
11
+ # furnished to do so, subject to the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be included in
14
+ # all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
+ # THE SOFTWARE.
23
+
24
+ module Timers
25
+ # A priority queue implementation using a standard binary minheap. It uses straight comparison
26
+ # of its contents to determine priority. This works because a Handle from Timers::Events implements
27
+ # the '<' operation by comparing the expiry time.
28
+ # See <https://en.wikipedia.org/wiki/Binary_heap> for explanations of the main methods.
29
+ class PriorityHeap
30
+ def initialize
31
+ # The heap is represented with an array containing a binary tree. See
32
+ # https://en.wikipedia.org/wiki/Binary_heap#Heap_implementation for how this array
33
+ # is built up.
34
+ @contents = []
35
+ end
36
+
37
+ # Returns the earliest timer or nil if the heap is empty.
38
+ def peek
39
+ @contents[0]
40
+ end
41
+
42
+ # Returns the number of elements in the heap
43
+ def size
44
+ @contents.size
45
+ end
46
+
47
+ # Returns the earliest timer if the heap is non-empty and removes it from the heap.
48
+ # Returns nil if the heap is empty. (and doesn't change the heap in that case)
49
+ def pop
50
+ # If the heap is empty:
51
+ if @contents.empty?
52
+ return nil
53
+ end
54
+
55
+ # If we have only one item, no swapping is required:
56
+ if @contents.size == 1
57
+ return @contents.pop
58
+ end
59
+
60
+ # Take the root of the tree:
61
+ value = @contents[0]
62
+
63
+ # Remove the last item in the tree:
64
+ last = @contents.pop
65
+
66
+ # Overwrite the root of the tree with the item:
67
+ @contents[0] = last
68
+
69
+ # Bubble it down into place:
70
+ bubble_down(0)
71
+
72
+ # validate!
73
+
74
+ return value
75
+ end
76
+
77
+ # Inserts a new timer into the heap, then rearranges elements until the heap invariant is true again.
78
+ def push(element)
79
+ # Insert the item at the end of the heap:
80
+ @contents.push(element)
81
+
82
+ # Bubble it up into position:
83
+ bubble_up(@contents.size - 1)
84
+
85
+ # validate!
86
+
87
+ return self
88
+ end
89
+
90
+ private
91
+
92
+ # Validate the heap invariant.
93
+ def validate!(index = 0)
94
+ if value = @contents[index]
95
+ left_index = index*2 + 1
96
+ if left_value = @contents[left_index]
97
+ unless value < left_value
98
+ raise "Invalid left index from #{index}!"
99
+ end
100
+
101
+ validate!(left_index)
102
+ end
103
+
104
+ right_index = left_index + 1
105
+ if right_value = @contents[right_index]
106
+ unless value < right_value
107
+ raise "Invalid right index from #{index}!"
108
+ end
109
+
110
+ validate!(right_index)
111
+ end
112
+ end
113
+ end
114
+
115
+ def swap(i, j)
116
+ @contents[i], @contents[j] = @contents[j], @contents[i]
117
+ end
118
+
119
+ def bubble_up(index)
120
+ parent_index = (index - 1) / 2 # watch out, integer division!
121
+
122
+ while index > 0 && @contents[index] < @contents[parent_index]
123
+ # if the node has a smaller value than its parent, swap these nodes
124
+ # to uphold the minheap invariant and update the index of the 'current'
125
+ # node. If the node is already at index 0, we can also stop because that
126
+ # is the root of the heap.
127
+ # swap(index, parent_index)
128
+ @contents[index], @contents[parent_index] = @contents[parent_index], @contents[index]
129
+
130
+ index = parent_index
131
+ parent_index = (index - 1) / 2 # watch out, integer division!
132
+ end
133
+ end
134
+
135
+ def bubble_down(index)
136
+ swap_value = 0
137
+ swap_index = nil
138
+
139
+ while true
140
+ left_index = (2 * index) + 1
141
+ left_value = @contents[left_index]
142
+
143
+ if left_value.nil?
144
+ # This node has no children so it can't bubble down any further.
145
+ # We're done here!
146
+ return
147
+ end
148
+
149
+ # Determine which of the child nodes has the smallest value:
150
+ right_index = left_index + 1
151
+ right_value = @contents[right_index]
152
+
153
+ if right_value.nil? or right_value > left_value
154
+ swap_value = left_value
155
+ swap_index = left_index
156
+ else
157
+ swap_value = right_value
158
+ swap_index = right_index
159
+ end
160
+
161
+ if @contents[index] < swap_value
162
+ # No need to swap, the minheap invariant is already satisfied:
163
+ return
164
+ else
165
+ # At least one of the child node has a smaller value than the current node, swap current node with that child and update current node for if it might need to bubble down even further:
166
+ # swap(index, swap_index)
167
+ @contents[index], @contents[swap_index] = @contents[swap_index], @contents[index]
168
+
169
+ index = swap_index
170
+ end
171
+ end
172
+ end
173
+ end
174
+ end
data/lib/timers/timer.rb CHANGED
@@ -29,66 +29,66 @@ module Timers
29
29
  class Timer
30
30
  include Comparable
31
31
  attr_reader :interval, :offset, :recurring
32
-
32
+
33
33
  def initialize(group, interval, recurring = false, offset = nil, &block)
34
34
  @group = group
35
-
35
+
36
36
  @interval = interval
37
37
  @recurring = recurring
38
38
  @block = block
39
39
  @offset = offset
40
-
40
+
41
41
  @handle = nil
42
-
42
+
43
43
  # If a start offset was supplied, use that, otherwise use the current timers offset.
44
44
  reset(@offset || @group.current_offset)
45
45
  end
46
-
46
+
47
47
  def paused?
48
48
  @group.paused_timers.include? self
49
49
  end
50
-
50
+
51
51
  def pause
52
52
  return if paused?
53
-
53
+
54
54
  @group.timers.delete self
55
55
  @group.paused_timers.add self
56
-
56
+
57
57
  @handle.cancel! if @handle
58
58
  @handle = nil
59
59
  end
60
-
60
+
61
61
  def resume
62
62
  return unless paused?
63
-
63
+
64
64
  @group.paused_timers.delete self
65
-
65
+
66
66
  # This will add us back to the group:
67
67
  reset
68
68
  end
69
-
69
+
70
70
  alias continue resume
71
-
71
+
72
72
  # Extend this timer
73
73
  def delay(seconds)
74
74
  @handle.cancel! if @handle
75
-
75
+
76
76
  @offset += seconds
77
-
77
+
78
78
  @handle = @group.events.schedule(@offset, self)
79
79
  end
80
-
80
+
81
81
  # Cancel this timer. Do not call while paused.
82
82
  def cancel
83
83
  return unless @handle
84
-
84
+
85
85
  @handle.cancel! if @handle
86
86
  @handle = nil
87
-
87
+
88
88
  # This timer is no longer valid:
89
89
  @group.timers.delete self if @group
90
90
  end
91
-
91
+
92
92
  # Reset this timer. Do not call while paused.
93
93
  # @param offset [Numeric] the duration to add to the timer.
94
94
  def reset(offset = @group.current_offset)
@@ -99,12 +99,12 @@ module Timers
99
99
  else
100
100
  @group.timers << self
101
101
  end
102
-
102
+
103
103
  @offset = Float(offset) + @interval
104
-
104
+
105
105
  @handle = @group.events.schedule(@offset, self)
106
106
  end
107
-
107
+
108
108
  # Fire the block.
109
109
  def fire(offset = @group.current_offset)
110
110
  if recurring == :strict
@@ -115,36 +115,38 @@ module Timers
115
115
  else
116
116
  @offset = offset
117
117
  end
118
-
118
+
119
119
  @block.call(offset, self)
120
-
120
+
121
121
  cancel unless recurring
122
122
  end
123
-
123
+
124
124
  alias call fire
125
-
125
+
126
126
  # Number of seconds until next fire / since last fire
127
127
  def fires_in
128
128
  @offset - @group.current_offset if @offset
129
129
  end
130
-
130
+
131
131
  # Inspect a timer
132
132
  def inspect
133
- str = "#{to_s[0..-2]} ".dup
134
-
133
+ buffer = "#{to_s[0..-2]} ".dup
134
+
135
135
  if @offset
136
- str << if fires_in >= 0
137
- "fires in #{fires_in} seconds"
138
- else
139
- "fired #{fires_in.abs} seconds ago"
140
- end
141
-
142
- str << ", recurs every #{interval}" if recurring
136
+ if fires_in >= 0
137
+ buffer << "fires in #{fires_in} seconds"
138
+ else
139
+ buffer << "fired #{fires_in.abs} seconds ago"
140
+ end
141
+
142
+ buffer << ", recurs every #{interval}" if recurring
143
143
  else
144
- str << "dead"
144
+ buffer << "dead"
145
145
  end
146
-
147
- str << ">"
146
+
147
+ buffer << ">"
148
+
149
+ return buffer
148
150
  end
149
151
  end
150
152
  end
@@ -21,5 +21,5 @@
21
21
  # THE SOFTWARE.
22
22
 
23
23
  module Timers
24
- VERSION = "4.3.2"
24
+ VERSION = "4.3.3"
25
25
  end
data/lib/timers/wait.rb CHANGED
@@ -28,7 +28,7 @@ module Timers
28
28
  def self.for(duration, &block)
29
29
  if duration
30
30
  timeout = new(duration)
31
-
31
+
32
32
  timeout.while_time_remaining(&block)
33
33
  else
34
34
  loop do
@@ -36,31 +36,31 @@ module Timers
36
36
  end
37
37
  end
38
38
  end
39
-
39
+
40
40
  def initialize(duration)
41
41
  @duration = duration
42
42
  @remaining = true
43
43
  end
44
-
44
+
45
45
  attr_reader :duration
46
46
  attr_reader :remaining
47
-
47
+
48
48
  # Yields while time remains for work to be done:
49
49
  def while_time_remaining
50
50
  @interval = Interval.new
51
51
  @interval.start
52
-
52
+
53
53
  yield @remaining while time_remaining?
54
54
  ensure
55
55
  @interval.stop
56
56
  @interval = nil
57
57
  end
58
-
58
+
59
59
  private
60
-
60
+
61
61
  def time_remaining?
62
62
  @remaining = (@duration - @interval.to_f)
63
-
63
+
64
64
  @remaining > 0
65
65
  end
66
66
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timers
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.3.2
4
+ version: 4.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2020-09-04 00:00:00.000000000 Z
12
+ date: 2021-02-13 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: bundler
@@ -63,6 +63,7 @@ files:
63
63
  - lib/timers/events.rb
64
64
  - lib/timers/group.rb
65
65
  - lib/timers/interval.rb
66
+ - lib/timers/priority_heap.rb
66
67
  - lib/timers/timer.rb
67
68
  - lib/timers/version.rb
68
69
  - lib/timers/wait.rb