bonito 0.1.0
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 +7 -0
- data/.codeclimate.yml +8 -0
- data/.gitignore +17 -0
- data/.reek.yml +3 -0
- data/.rspec +2 -0
- data/.rubocop.yml +9 -0
- data/.ruby-gemset +1 -0
- data/.travis.yml +30 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +8 -0
- data/LICENSE.txt +21 -0
- data/README.md +159 -0
- data/Rakefile +15 -0
- data/_config.yml +1 -0
- data/bin/console +16 -0
- data/bin/setup +8 -0
- data/bonito.gemspec +45 -0
- data/lib/bonito.rb +8 -0
- data/lib/bonito/moment.rb +39 -0
- data/lib/bonito/parallel_timeline.rb +93 -0
- data/lib/bonito/progress.rb +91 -0
- data/lib/bonito/runner.rb +45 -0
- data/lib/bonito/scope.rb +73 -0
- data/lib/bonito/serial_timeline.rb +330 -0
- data/lib/bonito/timeline.rb +94 -0
- data/lib/bonito/version.rb +5 -0
- metadata +251 -0
data/lib/bonito.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bonito/timeline'
|
4
|
+
|
5
|
+
module Bonito
|
6
|
+
class MomentScheduler < Scheduler # :nodoc:
|
7
|
+
def each
|
8
|
+
yield ScopedMoment.new(timeline, starting_offset, scope)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# A Moment represents a single instant in time in which events may occur.
|
13
|
+
# Scheduler classes may be used in order to yield a sequence of
|
14
|
+
# Moment objects, each of which has been decorated with a Scope object,
|
15
|
+
# within the context of which the events defined in the Moment will be
|
16
|
+
# evaluated, as well as an Integer offset representing a number of
|
17
|
+
# seconds from some arbitrary start point.
|
18
|
+
#
|
19
|
+
# Such a Scheduler object may be passed to a Runner, along with some fixed
|
20
|
+
# starting point. The runner can the be used to evaluate the events defined
|
21
|
+
# in each of the scheduled Moment objects, simulating the time at which they
|
22
|
+
# occur to be that of the starting point plus the offset.
|
23
|
+
class Moment < Timeline
|
24
|
+
schedule_with MomentScheduler
|
25
|
+
|
26
|
+
# Initialises a new Moment
|
27
|
+
# [block]
|
28
|
+
# A Proc that will be evaluated at some simulated point in time by a
|
29
|
+
# Runner
|
30
|
+
def initialize(&block)
|
31
|
+
@block = block
|
32
|
+
super 0
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_proc # :nodoc:
|
36
|
+
@block
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bonito/timeline'
|
4
|
+
require 'bonito/serial_timeline'
|
5
|
+
require 'algorithms'
|
6
|
+
|
7
|
+
module Bonito
|
8
|
+
class ParallelScheduler < Scheduler # :nodoc:
|
9
|
+
def initialize(parallel, starting_offset, scope, opts = {})
|
10
|
+
super
|
11
|
+
@schedulers = parallel.map do |timeline|
|
12
|
+
timeline.schedule(starting_offset, scope, opts).to_enum
|
13
|
+
end
|
14
|
+
@heap = LazyMinHeap.new(*@schedulers)
|
15
|
+
end
|
16
|
+
|
17
|
+
def each
|
18
|
+
@heap.each { |moment| yield moment }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
class ParallelTimeline < Timeline # :nodoc:
|
23
|
+
schedule_with ParallelScheduler
|
24
|
+
|
25
|
+
def initialize(&block)
|
26
|
+
super 0
|
27
|
+
instance_eval(&block) if block_given?
|
28
|
+
end
|
29
|
+
|
30
|
+
def over(duration, after: 0, &block)
|
31
|
+
use Bonito::SerialTimeline.new(duration, &block), after: after
|
32
|
+
end
|
33
|
+
|
34
|
+
def also(over: duration, after: 0, &block)
|
35
|
+
over(over, after: after, &block)
|
36
|
+
end
|
37
|
+
|
38
|
+
def use(*timelines, after: 0)
|
39
|
+
timelines.each do |timeline|
|
40
|
+
send :<<, OffsetTimeline.new(timeline, after)
|
41
|
+
end
|
42
|
+
self
|
43
|
+
end
|
44
|
+
|
45
|
+
def repeat(times:, over:, after: 0, &block)
|
46
|
+
times.times { over(over, after: after, &block) }
|
47
|
+
self
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def <<(offset_timeline)
|
53
|
+
super offset_timeline
|
54
|
+
self.duration = [
|
55
|
+
duration, offset_timeline.offset + offset_timeline.duration
|
56
|
+
].max
|
57
|
+
self
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
class LazyMinHeap # :nodoc:
|
62
|
+
include Enumerable
|
63
|
+
|
64
|
+
def initialize(*sorted_enums)
|
65
|
+
@heap = Containers::MinHeap.new []
|
66
|
+
@enums = Set[*sorted_enums]
|
67
|
+
@enums.each(&method(:push_from_enum))
|
68
|
+
end
|
69
|
+
|
70
|
+
def pop
|
71
|
+
moment = @heap.next_key
|
72
|
+
enum = @heap.pop
|
73
|
+
push_from_enum enum
|
74
|
+
moment
|
75
|
+
end
|
76
|
+
|
77
|
+
def empty?
|
78
|
+
@enums.empty? && @heap.empty?
|
79
|
+
end
|
80
|
+
|
81
|
+
def each
|
82
|
+
yield pop until empty?
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def push_from_enum(enum)
|
88
|
+
@heap.push enum.next, enum
|
89
|
+
rescue StopIteration
|
90
|
+
@enums.delete enum
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'ruby-progressbar'
|
4
|
+
module Bonito
|
5
|
+
module ProgressCounter # :nodoc:
|
6
|
+
attr_reader :total
|
7
|
+
attr_reader :current
|
8
|
+
|
9
|
+
class Unknown # :nodoc:
|
10
|
+
include Singleton
|
11
|
+
def to_s
|
12
|
+
'-'
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
module ClassMethods # :nodoc:
|
17
|
+
def factory(*args)
|
18
|
+
->(total: nil, prefix: nil) { new(*args, total: total, prefix: prefix) }
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.included(base)
|
23
|
+
base.extend ClassMethods
|
24
|
+
end
|
25
|
+
|
26
|
+
def setup(total: ProgressCounter::Unknown.instance, prefix: nil)
|
27
|
+
@total = total
|
28
|
+
@prefix = prefix
|
29
|
+
@current = 0
|
30
|
+
end
|
31
|
+
|
32
|
+
def increment(change)
|
33
|
+
@current += change
|
34
|
+
on_increment change
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def to_s
|
39
|
+
"#{prefix} #{current} / #{total}"
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
def prefix
|
45
|
+
@prefix ||= "#{self.class}{#{object_id}} : Progress Made :"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
class ProgressLogger # :nodoc:
|
50
|
+
include ProgressCounter
|
51
|
+
|
52
|
+
def initialize(logger = Logger.new(STDOUT), **opts)
|
53
|
+
@logger = logger
|
54
|
+
setup opts
|
55
|
+
end
|
56
|
+
|
57
|
+
def on_increment(_increment)
|
58
|
+
@logger.info to_s
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
class ProgressBar # :nodoc:
|
63
|
+
include ProgressCounter
|
64
|
+
|
65
|
+
def initialize(**opts)
|
66
|
+
@bar = ::ProgressBar.create opts
|
67
|
+
@bar.total = opts[:total]
|
68
|
+
setup opts
|
69
|
+
end
|
70
|
+
|
71
|
+
def on_increment(increment)
|
72
|
+
increment.times { @bar.increment }
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
class ProgressDecorator < SimpleDelegator # :nodoc:
|
77
|
+
def initialize(enumerable, progress)
|
78
|
+
@progress = progress
|
79
|
+
super enumerable
|
80
|
+
end
|
81
|
+
|
82
|
+
def each
|
83
|
+
return to_enum(:each) unless block_given?
|
84
|
+
|
85
|
+
__getobj__.each do |item|
|
86
|
+
yield item
|
87
|
+
@progress.increment 1
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'timecop'
|
4
|
+
module Bonito # :nodoc:
|
5
|
+
class Runner # :nodoc:
|
6
|
+
def initialize(enumerable, opts = {})
|
7
|
+
@enumerable = enumerable
|
8
|
+
@opts = opts
|
9
|
+
end
|
10
|
+
|
11
|
+
def live?
|
12
|
+
@opts.fetch(:live) { false }
|
13
|
+
end
|
14
|
+
|
15
|
+
def daemonise?
|
16
|
+
@opts.fetch(:daemonise) { false }
|
17
|
+
end
|
18
|
+
|
19
|
+
def call
|
20
|
+
Process.daemon if daemonise?
|
21
|
+
@enumerable.each do |moment|
|
22
|
+
maybe_sleep moment
|
23
|
+
moment.evaluate
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def maybe_sleep(moment)
|
30
|
+
return unless live? && (nap_time = moment.offset - Time.now).positive?
|
31
|
+
|
32
|
+
sleep nap_time
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.run(
|
37
|
+
serial, starting:, scope: Scope.new,
|
38
|
+
progress_factory: ProgressLogger.factory, **opts
|
39
|
+
)
|
40
|
+
scheduler = serial.scheduler(starting, scope, opts)
|
41
|
+
progress = progress_factory.call total: scheduler.count
|
42
|
+
scheduler = ProgressDecorator.new scheduler, progress
|
43
|
+
Runner.new(scheduler, opts).call
|
44
|
+
end
|
45
|
+
end
|
data/lib/bonito/scope.rb
ADDED
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Bonito
|
4
|
+
class Accessor # :nodoc:
|
5
|
+
def initialize(scope, symbol)
|
6
|
+
@scope = scope
|
7
|
+
@symbol = symbol
|
8
|
+
@instance_var = :"@#{@symbol.to_s.chomp('=')}"
|
9
|
+
end
|
10
|
+
|
11
|
+
def access(*args)
|
12
|
+
assignment? ? set(*args) : get
|
13
|
+
end
|
14
|
+
|
15
|
+
# :reek:NilCheck
|
16
|
+
# String#match? Unavailable for Ruby 2.3
|
17
|
+
def assignment?
|
18
|
+
!@symbol.to_s.match(/\w+=/).nil?
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def get
|
24
|
+
scope = @scope
|
25
|
+
while scope
|
26
|
+
if scope.instance_variable_defined? @instance_var
|
27
|
+
return scope.instance_variable_get @instance_var
|
28
|
+
end
|
29
|
+
|
30
|
+
scope = scope.parent
|
31
|
+
end
|
32
|
+
raise NameError
|
33
|
+
end
|
34
|
+
|
35
|
+
def set(value)
|
36
|
+
@scope.instance_variable_set @instance_var, value
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
class Scope # :nodoc:
|
41
|
+
def initialize(parent = nil)
|
42
|
+
@parent = parent
|
43
|
+
end
|
44
|
+
|
45
|
+
def push
|
46
|
+
self.class.new self
|
47
|
+
end
|
48
|
+
|
49
|
+
protected
|
50
|
+
|
51
|
+
attr_reader :parent
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def method_missing(symbol, *args)
|
56
|
+
Accessor.new(self, symbol).access(*args)
|
57
|
+
rescue NameError
|
58
|
+
super
|
59
|
+
end
|
60
|
+
|
61
|
+
# :reek:BooleanParameter
|
62
|
+
# Inherits interface from Object#respond_to_missing?
|
63
|
+
def respond_to_missing?(symbol, respond_to_private = false)
|
64
|
+
accessor = Accessor.new(self, symbol)
|
65
|
+
return true if accessor.assignment?
|
66
|
+
|
67
|
+
accessor.access
|
68
|
+
true
|
69
|
+
rescue NameError
|
70
|
+
super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,330 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'bonito/timeline'
|
4
|
+
require 'bonito/moment'
|
5
|
+
require 'securerandom'
|
6
|
+
require 'timecop'
|
7
|
+
|
8
|
+
module Bonito # :nodoc:
|
9
|
+
class SerialScheduler < Scheduler # :nodoc:
|
10
|
+
def initialize(serial, starting_offset, parent_scope, opts = {})
|
11
|
+
super serial, starting_offset, parent_scope.push, opts
|
12
|
+
@distribution = Distribution.new starting_offset, serial, opts
|
13
|
+
end
|
14
|
+
|
15
|
+
# :reek:NestedIterators:
|
16
|
+
# Not sure how this can be avoided nicely at the moment
|
17
|
+
def each
|
18
|
+
@distribution.each do |timeline, offset|
|
19
|
+
timeline.scheduler(offset, scope, opts).each do |moment|
|
20
|
+
yield moment
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# A SerialTimeline is a data structure with a duration (measured in seconds)
|
27
|
+
# that contains +timelines+. A +timeline+ is any instance of a class that
|
28
|
+
# inherits from the Timeline base class.
|
29
|
+
#
|
30
|
+
# A SerialTimeline serves to define an interval in which it may be
|
31
|
+
# _simulated_ that one or more Moment objects are _evaluated_ _in_ series_.
|
32
|
+
#
|
33
|
+
# A SerialTimeline exposes methods that can either be used to define these
|
34
|
+
# Moment objects directly or to create additional _child_ data structures
|
35
|
+
# (i.e ParallelTimeline objects or further, child SerialTimeline objects)
|
36
|
+
# which can in turn be provide more fine grained control over precisely _when_
|
37
|
+
# any given Moment objects may be evaluated.
|
38
|
+
#
|
39
|
+
# === Example
|
40
|
+
#
|
41
|
+
# Bonito.over(2.weeks) do
|
42
|
+
# please { puts Time.now }
|
43
|
+
# end
|
44
|
+
#
|
45
|
+
# The above defines a SerialTimeline (using the Bonito#over module method)
|
46
|
+
# that specifies a 2 week time period. A single Moment is included in this
|
47
|
+
# serial (via the #please factory method). When the top level SerialTimeline
|
48
|
+
# is evaluated (using a Runner object) the block
|
49
|
+
#
|
50
|
+
# puts Time.now
|
51
|
+
#
|
52
|
+
# is evaluated _exactly_ _once_. Furthermore, the simulated time at which
|
53
|
+
# the block is evaluated will be contained at some point within the 2 week
|
54
|
+
# interval beginning on some start date provided when instantiating the
|
55
|
+
# Runner object.
|
56
|
+
#
|
57
|
+
# As mentioned, it is also possible to include other data structures within
|
58
|
+
# SerialTimeline objects, including other SerialTimeline objects.
|
59
|
+
#
|
60
|
+
# === Example
|
61
|
+
#
|
62
|
+
# We could use the #over method to add an empty SerialTimeline to the previous
|
63
|
+
# example in order to force the already included Moment to be evaluated
|
64
|
+
# during the last day of the 2 week period.
|
65
|
+
#
|
66
|
+
# Bonito.over(2.weeks) do
|
67
|
+
# over(2.weeks - 1.day) # This defines an empty serial
|
68
|
+
# please { puts Time.now }
|
69
|
+
# end
|
70
|
+
#
|
71
|
+
# The empty SerialTimeline returned by the #over factory method consumes 13
|
72
|
+
# days of the parent SerialTimeline object's total duration of 2 weeks. This
|
73
|
+
# means that when this parent SerialTimeline is evaluated, the Moment will be
|
74
|
+
# _as_ _if_ _it_ _occurred_ during the _final_ _day_ of the 2 week period.
|
75
|
+
#
|
76
|
+
# Finally, we may also define ParallelTimeline objects within serials using
|
77
|
+
# the #simultaneously method. These allow for multiple SerialTimeline
|
78
|
+
# objects to be defined over the same time period and for any Moment
|
79
|
+
# objects contained within to be _interleaved_ when the parent SerialTimeline
|
80
|
+
# is ultimately evaluated.
|
81
|
+
#
|
82
|
+
# The #simultaneously method instantiates a ParallelTimeline object, whilst
|
83
|
+
# accepting a block. The block is evaluated within the context of the new
|
84
|
+
# ParallelTimeline. Timelines defined within this block will be evaluated in
|
85
|
+
# parallel.
|
86
|
+
#
|
87
|
+
# Note that ParallelTimeline implements many of the same methods as
|
88
|
+
# SerialTimeline
|
89
|
+
#
|
90
|
+
# === Example
|
91
|
+
#
|
92
|
+
# Bonito.over(2.weeks) do
|
93
|
+
# simultaneously do
|
94
|
+
# over 1.week do
|
95
|
+
# puts "SerialTimeline 1 #{Time.now}"
|
96
|
+
# end
|
97
|
+
# over 6.days, after: 1.day do
|
98
|
+
# puts "SerialTimeline 2 #{Time.now}"
|
99
|
+
# end
|
100
|
+
# end
|
101
|
+
#
|
102
|
+
# over 1.week {} # This defines an empty serial
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# Now, when evaluating this SerialTimeline both the blocks
|
106
|
+
# puts "SerialTimeline 1 #{Time.now}"
|
107
|
+
# and
|
108
|
+
# puts "SerialTimeline 2 #{Time.now}"
|
109
|
+
# will be evaluated once during the first week. The precise instant is
|
110
|
+
# chosen randomly within this interval with the only constraint being
|
111
|
+
# that the second block cannot be evaluated during the first day (This
|
112
|
+
# offset is controlled by the +after+ parameter of the #simultaneously
|
113
|
+
# method).
|
114
|
+
#
|
115
|
+
# *Note* that the moment from the second SerialTimeline could still be
|
116
|
+
# evaluated at a simulated time _before_ that at which the moment from the
|
117
|
+
# first SerialTimeline is evaluated.
|
118
|
+
class SerialTimeline < Timeline
|
119
|
+
schedule_with SerialScheduler
|
120
|
+
|
121
|
+
# Instantiate a new SerialTimeline object
|
122
|
+
#
|
123
|
+
# [duration]
|
124
|
+
# The total time period (in seconds) that the
|
125
|
+
# SerialTimeline encompasses
|
126
|
+
# [parent]
|
127
|
+
# If the SerialTimeline is a child of another Timeline,
|
128
|
+
# parent is this Timeline
|
129
|
+
# [block]
|
130
|
+
# A block that will be evaluated within the context of
|
131
|
+
# the newly created SerialTimeline. Note that the following two
|
132
|
+
# statements are equivalent
|
133
|
+
#
|
134
|
+
# a_serial = Bonito::SerialTimeline.new(1.week) do
|
135
|
+
# please { p Time.now }
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# another_serial = Bonito::SerialTimeline.new 1.week
|
139
|
+
# serial.please { p Time.now }
|
140
|
+
#
|
141
|
+
# The ability to include a block in this way is in order to allow the
|
142
|
+
# code used to define a given SerialTimeline will reflect its hierarchy.
|
143
|
+
def initialize(duration, parent = nil, &block)
|
144
|
+
@parent = parent
|
145
|
+
@total_child_duration = 0
|
146
|
+
super duration
|
147
|
+
instance_eval(&block) if block_given?
|
148
|
+
end
|
149
|
+
|
150
|
+
# The the amount of #duration remaining taking into account the duration of
|
151
|
+
# any Timeline objects included as children of the SerialTimeline.
|
152
|
+
def unused_duration
|
153
|
+
duration - @total_child_duration
|
154
|
+
end
|
155
|
+
|
156
|
+
# Define a new SerialTimeline and add it as a child to the current
|
157
|
+
# SerialTimeline
|
158
|
+
#
|
159
|
+
# [duration]
|
160
|
+
# The duration (in seconds) of the newly created child SerialTimeline
|
161
|
+
#
|
162
|
+
# [block]
|
163
|
+
# A block passed to the #new method on the child SerialTimeline object
|
164
|
+
#
|
165
|
+
# Returns the newly created SerialTimeline object
|
166
|
+
def over(duration, &block)
|
167
|
+
self.class.new(duration, self, &block).tap(&method(:use))
|
168
|
+
end
|
169
|
+
|
170
|
+
# Define a new Moment and add it as a child to the current SerialTimeline
|
171
|
+
#
|
172
|
+
# [block]
|
173
|
+
# A block passed to the #new method on the child Moment object
|
174
|
+
#
|
175
|
+
# Returns the newly created Moment object
|
176
|
+
def please(&block)
|
177
|
+
Moment.new(&block).tap(&method(:use))
|
178
|
+
end
|
179
|
+
|
180
|
+
# Define a new serial and append it multiple times as a child of the
|
181
|
+
# current SerialTimeline object.
|
182
|
+
#
|
183
|
+
# [times]
|
184
|
+
# The number of times that the new SerialTimeline object to
|
185
|
+
# be appended to the current SerialTimeline
|
186
|
+
#
|
187
|
+
# [over]
|
188
|
+
# The total duration (in senconds) of the new
|
189
|
+
# repeated SerialTimeline objects.
|
190
|
+
#
|
191
|
+
# [block]
|
192
|
+
# A block passed to the #new method on the child SerialTimeline
|
193
|
+
# object
|
194
|
+
#
|
195
|
+
# Returns the current SerialTimeline
|
196
|
+
def repeat(times:, over:, &block)
|
197
|
+
repeated_block = proc { times.times { instance_eval(&block) } }
|
198
|
+
over(over, &repeated_block)
|
199
|
+
end
|
200
|
+
|
201
|
+
# Define a new ParallelTimeline object append it as a child to the current
|
202
|
+
# SerialTimeline. Also permit the evaluation of methods within the context
|
203
|
+
# of the new ParallelTimeline.
|
204
|
+
#
|
205
|
+
# [block]
|
206
|
+
# A block to be passed to the #new method on the child ParallelTimeline
|
207
|
+
# method.
|
208
|
+
#
|
209
|
+
# Returns the current SerialTimeline object
|
210
|
+
def simultaneously(&block)
|
211
|
+
use ParallelTimeline.new(&block)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Append an existing Timeline as a child of the current SerialTimeline
|
215
|
+
#
|
216
|
+
# [timelines]
|
217
|
+
# An array of Timeline objects that will be
|
218
|
+
# appended, in order to the current SerialTimeline
|
219
|
+
#
|
220
|
+
# Returns the current SerialTimeline object
|
221
|
+
def use(*timelines)
|
222
|
+
timelines.each { |timeline| send :<<, timeline }
|
223
|
+
self
|
224
|
+
end
|
225
|
+
|
226
|
+
# Combine two Windows into a single, larger SerialTimeline object.
|
227
|
+
#
|
228
|
+
# [other]
|
229
|
+
# Some other SerialTimeline object
|
230
|
+
#
|
231
|
+
# Returns a SerialTimeline object consisting of the ordered child Timeline
|
232
|
+
# objects of the current SerialTimeline with the ordered child Timeline
|
233
|
+
# objects of +other+ appended to the end.
|
234
|
+
def +(other)
|
235
|
+
SerialTimeline.new duration + other.duration do
|
236
|
+
use(*(to_a + other.to_a))
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
# Repeatedly apply the #+ method of the current SerialTimeline to itself
|
241
|
+
#
|
242
|
+
# [other]
|
243
|
+
# Denotes the number of times the current serial
|
244
|
+
# should be added to itself.
|
245
|
+
#
|
246
|
+
# Returns a new SerialTimeline object
|
247
|
+
#
|
248
|
+
# Note that the following statements are equivalent for
|
249
|
+
# some serial +serial+:
|
250
|
+
#
|
251
|
+
# serial * 3
|
252
|
+
# serial + serial + serial
|
253
|
+
#
|
254
|
+
def *(other)
|
255
|
+
SerialTimeline.new(duration * other) do
|
256
|
+
use(*Array.new(other) { entries }.reduce(:+))
|
257
|
+
end
|
258
|
+
end
|
259
|
+
|
260
|
+
# Scale up a serial by parallelising it according to some factor
|
261
|
+
#
|
262
|
+
# [other]
|
263
|
+
# An Integer denoting the degree of parallelism with
|
264
|
+
# which to scale the serial.
|
265
|
+
#
|
266
|
+
# Returns a new ParallelTimeline whose child timelines are precisely
|
267
|
+
# the current serial repeated +other+ times.
|
268
|
+
def **(other)
|
269
|
+
this = self
|
270
|
+
SerialTimeline.new(duration) do
|
271
|
+
simultaneously { use(*Array.new(other) { this }) }
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
private
|
276
|
+
|
277
|
+
def <<(timeline)
|
278
|
+
tap do
|
279
|
+
@total_child_duration += timeline.duration
|
280
|
+
if @total_child_duration > duration
|
281
|
+
raise WindowDurationExceeded, "#{@total_child_duration} > #{duration}"
|
282
|
+
end
|
283
|
+
|
284
|
+
super timeline
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
|
289
|
+
class BonitoException < StandardError
|
290
|
+
end
|
291
|
+
|
292
|
+
class WindowDurationExceeded < BonitoException
|
293
|
+
end
|
294
|
+
|
295
|
+
def self.over(duration, &block)
|
296
|
+
SerialTimeline.new duration, &block
|
297
|
+
end
|
298
|
+
|
299
|
+
class Distribution # :nodoc:
|
300
|
+
include Enumerable
|
301
|
+
|
302
|
+
def initialize(start, serial, stretch: 1)
|
303
|
+
@start = start
|
304
|
+
@serial = serial
|
305
|
+
@stretch = stretch
|
306
|
+
end
|
307
|
+
|
308
|
+
def each
|
309
|
+
@serial.zip(generate_offsets).reduce(0) do |consumed, zipped|
|
310
|
+
timeline, offset = zipped
|
311
|
+
yield timeline, @start + (@stretch * (offset + consumed))
|
312
|
+
consumed + timeline.duration
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
private
|
317
|
+
|
318
|
+
def interval
|
319
|
+
@serial.unused_duration
|
320
|
+
end
|
321
|
+
|
322
|
+
def size
|
323
|
+
@serial.size
|
324
|
+
end
|
325
|
+
|
326
|
+
def generate_offsets
|
327
|
+
Array.new(size) { SecureRandom.random_number(interval) }.sort
|
328
|
+
end
|
329
|
+
end
|
330
|
+
end
|