timers 4.3.2 → 4.3.3

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.
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