timers 4.2.0 → 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: e1665f074c230a801e0eb6648214a6d0248d4baada8da1f4e60abd0983bba517
4
- data.tar.gz: de4eac12ecba1826d6ae5ef1d6958a451595b1a9e8e54dba7f1718d7f15182dd
3
+ metadata.gz: a9f471bfb9cac57f9462498ab12527eded7533318cec13604a10d32b159c6689
4
+ data.tar.gz: 69802b0774101037596274f2acf114c3f4234875ed8a82146e8ef1075407bf6d
5
5
  SHA512:
6
- metadata.gz: c0e005f02975d324b7fbe7d8e284a2d5318be78352e129ce00abf20594d3e6a18b30103e1137b2d9fe91bd5a2251e7b22fb5cdc255f5153f219bf90bd3b6160d
7
- data.tar.gz: 64210a85d0a8efe2540d9ee6da050c5d8298a5332bd2ba14758bd784a7f991ad40b1f828b47320525ab025249859ca6419f172421492fe6ced56a76e34820541
6
+ metadata.gz: f15934803658ec4b1d0d7b4a8ea92c9a21e2267544ec11c9619604288264c7f0add19dd12b71a0680827d6d37b853192d98266f29b06ef27cb36e53408f8c67b
7
+ data.tar.gz: a50a935d7b73681b4b751341c6eda0143c64beb9d469641ea68b8481cd1c36bd16ad21f7ec274f06d1389b53c5849e06b20182d3faea6497f64773d8fba2dae6
data/lib/timers.rb CHANGED
@@ -1,11 +1,26 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # This file is part of the "timers" project and released under the MIT license.
4
- #
5
- # Copyright, 2018, by Samuel Williams. All rights reserved.
6
- #
7
2
 
8
- require "timers/version"
3
+ # Copyright, 2020, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
9
22
 
10
- require "timers/group"
11
- require "timers/wait"
23
+ require_relative "timers/version"
24
+
25
+ require_relative "timers/group"
26
+ require_relative "timers/wait"
data/lib/timers/events.rb CHANGED
@@ -1,113 +1,115 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # This file is part of the "timers" project and released under the MIT license.
4
- #
5
- # Copyright, 2018, by Samuel Williams. All rights reserved.
6
- #
2
+
3
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
7
22
 
8
23
  require_relative "timer"
24
+ require_relative "priority_heap"
9
25
 
10
26
  module Timers
11
- # Maintains an ordered list of events, which can be cancelled.
27
+ # Maintains a PriorityHeap of events ordered on time, which can be cancelled.
12
28
  class Events
13
29
  # Represents a cancellable handle for a specific timer event.
14
30
  class Handle
31
+ include Comparable
32
+
15
33
  def initialize(time, callback)
16
34
  @time = time
17
35
  @callback = callback
18
36
  end
19
-
37
+
20
38
  # The absolute time that the handle should be fired at.
21
39
  attr_reader :time
22
-
40
+
23
41
  # Cancel this timer, O(1).
24
42
  def cancel!
25
43
  # The simplest way to keep track of cancelled status is to nullify the
26
44
  # callback. This should also be optimal for garbage collection.
27
45
  @callback = nil
28
46
  end
29
-
47
+
30
48
  # Has this timer been cancelled? Cancelled timer's don't fire.
31
49
  def cancelled?
32
50
  @callback.nil?
33
51
  end
34
-
35
- def >(other)
36
- @time > other.to_f
37
- end
38
-
39
- def to_f
40
- @time
52
+
53
+ def <=> other
54
+ @time <=> other.time
41
55
  end
42
-
56
+
43
57
  # Fire the callback if not cancelled with the given time parameter.
44
58
  def fire(time)
45
59
  @callback.call(time) if @callback
46
60
  end
47
61
  end
48
-
62
+
49
63
  def initialize
50
64
  # A sequence of handles, maintained in sorted order, future to present.
51
65
  # @sequence.last is the next event to be fired.
52
- @sequence = []
66
+ @sequence = PriorityHeap.new
67
+ @queue = []
53
68
  end
54
-
69
+
55
70
  # Add an event at the given time.
56
71
  def schedule(time, callback)
57
72
  handle = Handle.new(time.to_f, callback)
58
-
59
- index = bisect_left(@sequence, handle)
60
-
61
- # Maintain sorted order, O(logN) insertion time.
62
- @sequence.insert(index, handle)
63
-
64
- handle
73
+
74
+ @queue << handle
75
+
76
+ return handle
65
77
  end
66
-
78
+
67
79
  # Returns the first non-cancelled handle.
68
80
  def first
69
- while (handle = @sequence.last)
81
+ merge!
82
+
83
+ while (handle = @sequence.peek)
70
84
  return handle unless handle.cancelled?
71
85
  @sequence.pop
72
86
  end
73
87
  end
74
-
88
+
75
89
  # Returns the number of pending (possibly cancelled) events.
76
90
  def size
77
- @sequence.size
91
+ @sequence.size + @queue.size
78
92
  end
79
-
93
+
80
94
  # Fire all handles for which Handle#time is less than the given time.
81
95
  def fire(time)
82
- pop(time).reverse_each do |handle|
96
+ merge!
97
+
98
+ while handle = @sequence.peek and handle.time <= time
99
+ @sequence.pop
83
100
  handle.fire(time)
84
101
  end
85
102
  end
86
-
103
+
87
104
  private
88
-
89
- # Efficiently take k handles for which Handle#time is less than the given
90
- # time.
91
- def pop(time)
92
- index = bisect_left(@sequence, time)
93
-
94
- @sequence.pop(@sequence.size - index)
95
- end
96
-
97
- # Return the left-most index where to insert item e, in a list a, assuming
98
- # a is sorted in descending order.
99
- def bisect_left(a, e, l = 0, u = a.length)
100
- while l < u
101
- m = l + (u - l).div(2)
102
-
103
- if a[m] > e
104
- l = m + 1
105
- else
106
- u = m
107
- end
105
+
106
+ # Move all non-cancelled timers from the pending queue to the priority heap
107
+ def merge!
108
+ while handle = @queue.pop
109
+ next if handle.cancelled?
110
+
111
+ @sequence.push(handle)
108
112
  end
109
-
110
- l
111
113
  end
112
114
  end
113
115
  end
data/lib/timers/group.rb CHANGED
@@ -1,9 +1,24 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # This file is part of the "timers" project and released under the MIT license.
4
- #
5
- # Copyright, 2018, by Samuel Williams. All rights reserved.
6
- #
2
+
3
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
7
22
 
8
23
  require "set"
9
24
  require "forwardable"
@@ -16,62 +31,62 @@ module Timers
16
31
  # A collection of timers which may fire at different times
17
32
  class Group
18
33
  include Enumerable
19
-
34
+
20
35
  extend Forwardable
21
36
  def_delegators :@timers, :each, :empty?
22
-
37
+
23
38
  def initialize
24
39
  @events = Events.new
25
-
40
+
26
41
  @timers = Set.new
27
42
  @paused_timers = Set.new
28
-
43
+
29
44
  @interval = Interval.new
30
45
  @interval.start
31
46
  end
32
-
47
+
33
48
  # Scheduled events:
34
49
  attr_reader :events
35
-
50
+
36
51
  # Active timers:
37
52
  attr_reader :timers
38
-
53
+
39
54
  # Paused timers:
40
55
  attr_reader :paused_timers
41
-
56
+
42
57
  # Call the given block after the given interval. The first argument will be
43
58
  # the time at which the group was asked to fire timers for.
44
59
  def after(interval, &block)
45
60
  Timer.new(self, interval, false, &block)
46
61
  end
47
-
62
+
48
63
  # Call the given block immediately, and then after the given interval. The first
49
64
  # argument will be the time at which the group was asked to fire timers for.
50
65
  def now_and_after(interval, &block)
51
66
  yield
52
67
  after(interval, &block)
53
68
  end
54
-
69
+
55
70
  # Call the given block periodically at the given interval. The first
56
71
  # argument will be the time at which the group was asked to fire timers for.
57
72
  def every(interval, recur = true, &block)
58
73
  Timer.new(self, interval, recur, &block)
59
74
  end
60
-
75
+
61
76
  # Call the given block immediately, and then periodically at the given interval. The first
62
77
  # argument will be the time at which the group was asked to fire timers for.
63
78
  def now_and_every(interval, recur = true, &block)
64
79
  yield
65
80
  every(interval, recur, &block)
66
81
  end
67
-
82
+
68
83
  # Wait for the next timer and fire it. Can take a block, which should behave
69
84
  # like sleep(n), except that n may be nil (sleep forever) or a negative
70
85
  # number (fire immediately after return).
71
86
  def wait
72
87
  if block_given?
73
88
  yield wait_interval
74
-
89
+
75
90
  while (interval = wait_interval) && interval > 0
76
91
  yield interval
77
92
  end
@@ -81,10 +96,10 @@ module Timers
81
96
  sleep interval
82
97
  end
83
98
  end
84
-
99
+
85
100
  fire
86
101
  end
87
-
102
+
88
103
  # Interval to wait until when the next timer will fire.
89
104
  # - nil: no timers
90
105
  # - -ve: timers expired already
@@ -94,36 +109,36 @@ module Timers
94
109
  handle = @events.first
95
110
  handle.time - Float(offset) if handle
96
111
  end
97
-
112
+
98
113
  # Fire all timers that are ready.
99
114
  def fire(offset = current_offset)
100
115
  @events.fire(offset)
101
116
  end
102
-
117
+
103
118
  # Pause all timers.
104
119
  def pause
105
120
  @timers.dup.each(&:pause)
106
121
  end
107
-
122
+
108
123
  # Resume all timers.
109
124
  def resume
110
125
  @paused_timers.dup.each(&:resume)
111
126
  end
112
-
127
+
113
128
  alias continue resume
114
-
129
+
115
130
  # Delay all timers.
116
131
  def delay(seconds)
117
132
  @timers.each do |timer|
118
133
  timer.delay(seconds)
119
134
  end
120
135
  end
121
-
136
+
122
137
  # Cancel all timers.
123
138
  def cancel
124
139
  @timers.dup.each(&:cancel)
125
140
  end
126
-
141
+
127
142
  # The group's current time.
128
143
  def current_offset
129
144
  @interval.to_f
@@ -1,9 +1,24 @@
1
1
  # frozen_string_literal: true
2
- #
3
- # This file is part of the "timers" project and released under the MIT license.
4
- #
5
- # Copyright, 2018, by Samuel Williams. All rights reserved.
6
- #
2
+
3
+ # Copyright, 2018, by Samuel G. D. Williams. <http://www.codeotaku.com>
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ # of this software and associated documentation files (the "Software"), to deal
7
+ # in the Software without restriction, including without limitation the rights
8
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ # copies of the Software, and to permit persons to whom the Software is
10
+ # furnished to do so, subject to the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be included in
13
+ # all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ # THE SOFTWARE.
7
22
 
8
23
  module Timers
9
24
  # A collection of timers which may fire at different times
@@ -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