madderlib 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.
- data/CHANGELOG +3 -0
- data/LICENSE +22 -0
- data/README.rdoc +173 -0
- data/Rakefile +134 -0
- data/lib/madderlib.rb +27 -0
- data/lib/madderlib/builder.rb +659 -0
- data/lib/madderlib/conditional/allowed.rb +144 -0
- data/lib/madderlib/conditional/helper.rb +135 -0
- data/lib/madderlib/conditional/likely.rb +162 -0
- data/lib/madderlib/conditional/recur.rb +103 -0
- data/lib/madderlib/conditional/registry.rb +56 -0
- data/lib/madderlib/conditional/repeat.rb +130 -0
- data/lib/madderlib/context.rb +140 -0
- data/lib/madderlib/core.rb +217 -0
- data/lib/madderlib/extensions.rb +40 -0
- data/lib/madderlib/instruction.rb +171 -0
- data/lib/madderlib/phrase.rb +284 -0
- data/lib/madderlib/sequencer.rb +337 -0
- data/madderlib.gemspec +72 -0
- data/spec/benchmark_spec.rb +69 -0
- data/spec/builder_spec.rb +321 -0
- data/spec/builder_to_other_spec.rb +47 -0
- data/spec/builder_to_sequencer_spec.rb +388 -0
- data/spec/conditional_allowed_spec.rb +130 -0
- data/spec/conditional_helper_spec.rb +131 -0
- data/spec/conditional_likely_spec.rb +138 -0
- data/spec/conditional_recur_spec.rb +102 -0
- data/spec/conditional_registry_spec.rb +94 -0
- data/spec/conditional_repeat_spec.rb +88 -0
- data/spec/doc_spec.rb +550 -0
- data/spec/error_spec.rb +33 -0
- data/spec/examples_spec.rb +151 -0
- data/spec/extensions_spec.rb +47 -0
- data/spec/grammar_spec.rb +101 -0
- data/spec/instruction_spec.rb +133 -0
- data/spec/kernel_spec.rb +58 -0
- data/spec/phrase_spec.rb +7 -0
- data/spec/sequencer_spec.rb +317 -0
- data/spec/spec_helper.rb +54 -0
- metadata +98 -0
@@ -0,0 +1,144 @@
|
|
1
|
+
module MadderLib
|
2
|
+
module Conditional
|
3
|
+
#= Allowed
|
4
|
+
#
|
5
|
+
#Introduces support for conditional usage of an Instruction
|
6
|
+
module Allowed
|
7
|
+
|
8
|
+
#= Allowed::Phrase
|
9
|
+
#
|
10
|
+
#Introduces support for conditional usage of an Instruction
|
11
|
+
#
|
12
|
+
#If a Phrase has no Instructions that can be used, it will return an empty result and be omitted
|
13
|
+
#
|
14
|
+
#See: Allowed::Instruction
|
15
|
+
module Phrase
|
16
|
+
#Adds conditional logic to the current Instruction
|
17
|
+
#
|
18
|
+
#See: Instruction#assuming
|
19
|
+
def assuming(*args, &block)
|
20
|
+
self.instruction.assuming *args, &block
|
21
|
+
end
|
22
|
+
alias :presuming :assuming
|
23
|
+
alias :if :assuming
|
24
|
+
|
25
|
+
#Adds conditional logic to the current Instruction
|
26
|
+
#
|
27
|
+
#See: Instruction#forbidding
|
28
|
+
def forbidding(*args, &block)
|
29
|
+
self.instruction.forbidding *args, &block
|
30
|
+
end
|
31
|
+
alias :unless :forbidding
|
32
|
+
end
|
33
|
+
|
34
|
+
|
35
|
+
|
36
|
+
#= Allowed::Instruction
|
37
|
+
#
|
38
|
+
#Introduces support for conditional usage of an Instruction
|
39
|
+
#
|
40
|
+
#The conditional logic is evaluated for each separate execution of the Builder.
|
41
|
+
#If the conditional returns false, then the Instruction is excluded from use.
|
42
|
+
#If it returns true, then the Instruction is still a viable candidate (though other constraints may be applied)
|
43
|
+
#
|
44
|
+
#See: Allowed::Phrase
|
45
|
+
module Instruction
|
46
|
+
def self.included(target) #:nodoc:
|
47
|
+
# register a test to test all allowances for the instruction
|
48
|
+
# return false at the first one that fails
|
49
|
+
target.add_test do |instruction, context|
|
50
|
+
failure = instruction.conditional_allowances.find do |block|
|
51
|
+
# first failure stops us
|
52
|
+
(! Context.invoke(block, context))
|
53
|
+
end
|
54
|
+
|
55
|
+
failure.nil?
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
#The instruction will only be used if these conditions are true
|
62
|
+
#
|
63
|
+
#The id of a Phrase can be provided.
|
64
|
+
#If so, then the condition tested is: the reference must have already been added to the Builder's result.
|
65
|
+
#This can be identified through Context#spoken
|
66
|
+
#
|
67
|
+
#Alternately, a custom block can be provided.
|
68
|
+
#The block can either take no arguments, or a Context.
|
69
|
+
#The Instruction may only be used when that block returns a true (eg. non-false) value
|
70
|
+
#
|
71
|
+
#Examples:
|
72
|
+
# switch = false
|
73
|
+
# builder = madderlib do
|
74
|
+
# an(:on).says('on').assuming { switch }
|
75
|
+
# an(:off).says('off').if { ! switch }
|
76
|
+
# say('bright').if :on
|
77
|
+
# say('dark').if :off
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# builder.sentence.should eql('off dark')
|
81
|
+
# switch = true
|
82
|
+
# builder.sentence.should eql('on bright')
|
83
|
+
def assuming(id=nil, &block)
|
84
|
+
if block
|
85
|
+
raise Error, 'block AND id provided, requires one or the other' if id
|
86
|
+
Context.validate(block)
|
87
|
+
else
|
88
|
+
# true if the id expressed has been spoken
|
89
|
+
block = lambda {|context| context.spoken_ids.include?(id) }
|
90
|
+
end
|
91
|
+
|
92
|
+
# set it aside for a lazy day
|
93
|
+
conditional_allowances << block
|
94
|
+
self
|
95
|
+
end
|
96
|
+
alias :presuming :assuming
|
97
|
+
alias :if :assuming
|
98
|
+
|
99
|
+
#The instruction will only be used if these conditions are false.
|
100
|
+
#This is the logical opposite of assuming.
|
101
|
+
#
|
102
|
+
#The id of a Phrase can be provided.
|
103
|
+
#If so, then the condition tested is: the reference must not have been added to the Builder's result.
|
104
|
+
#This can be identified through Context#spoken
|
105
|
+
#
|
106
|
+
#Alternately, a custom block can be provided.
|
107
|
+
#The block can either take no arguments, or a Context.
|
108
|
+
#The Instruction may only be used when that block returns a false value
|
109
|
+
#
|
110
|
+
#Examples:
|
111
|
+
# switch = false
|
112
|
+
# builder = madderlib do
|
113
|
+
# an(:on).says('on').forbidding { ! switch }
|
114
|
+
# an(:off).says('off').unless { switch }
|
115
|
+
# say('bright').unless :off
|
116
|
+
# say('dark').unless :on
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# builder.sentence.should eql('off dark')
|
120
|
+
# switch = true
|
121
|
+
# builder.sentence.should eql('on bright')
|
122
|
+
def forbidding(id=nil, &block)
|
123
|
+
if block
|
124
|
+
raise Error, 'block AND id provided, requires one or the other' if id
|
125
|
+
Context.validate(block)
|
126
|
+
|
127
|
+
self.assuming {|context| ! Context.invoke(block, context) }
|
128
|
+
else
|
129
|
+
# true unless the id expressed has been spoken
|
130
|
+
self.assuming {|context| ! context.spoken_ids.include?(id) }
|
131
|
+
end
|
132
|
+
end
|
133
|
+
alias :unless :forbidding
|
134
|
+
|
135
|
+
|
136
|
+
|
137
|
+
def conditional_allowances #:nodoc:
|
138
|
+
@conditional_allowances ||= []
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,135 @@
|
|
1
|
+
module MadderLib
|
2
|
+
module Conditional
|
3
|
+
module Helper #:nodoc: all
|
4
|
+
|
5
|
+
class TestBlock
|
6
|
+
FALSE = lambda { false }
|
7
|
+
ONE = lambda { 1 }
|
8
|
+
|
9
|
+
attr_reader :criteria
|
10
|
+
|
11
|
+
def initialize(*args, &block)
|
12
|
+
if block
|
13
|
+
# if we get a block, use it!
|
14
|
+
raise Error, 'block AND args provided, requires one or the other' if (args && (! args.empty?))
|
15
|
+
raise Error, 'block arity should be 0; 1 (count) or; 2 (count, Context)' if (block.arity > 2)
|
16
|
+
|
17
|
+
# it will remain unchanging
|
18
|
+
@criteria = block
|
19
|
+
else
|
20
|
+
# leave the originals alone
|
21
|
+
args = args.clone
|
22
|
+
|
23
|
+
begin
|
24
|
+
# how does it start?
|
25
|
+
arg = args.shift
|
26
|
+
|
27
|
+
if Range === arg
|
28
|
+
# we received a Range
|
29
|
+
@criteria = arg
|
30
|
+
elsif arg.respond_to?(:integer?) && arg.integer?
|
31
|
+
upper = args.first
|
32
|
+
if upper && upper.respond_to?(:integer?) && upper.integer?
|
33
|
+
# we can make a Range from that
|
34
|
+
@criteria = Range.new(arg, args.shift)
|
35
|
+
else
|
36
|
+
# just a count
|
37
|
+
@criteria = arg
|
38
|
+
end
|
39
|
+
else
|
40
|
+
raise Error, "invalid test block argument : #{arg.inspect}"
|
41
|
+
end
|
42
|
+
rescue Error => e
|
43
|
+
raise e
|
44
|
+
rescue Exception => e
|
45
|
+
# wrap
|
46
|
+
raise e
|
47
|
+
raise Error.new("invalid test block argument : #{arg.inspect}", e)
|
48
|
+
end
|
49
|
+
|
50
|
+
# beyond that, is there a unit?
|
51
|
+
# we deal with all of that during build
|
52
|
+
@units = args.shift
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
|
57
|
+
|
58
|
+
def block
|
59
|
+
if Proc === @criteria
|
60
|
+
# the block will do its own testing
|
61
|
+
@criteria
|
62
|
+
elsif Range === @criteria
|
63
|
+
# we'll stop somewhere in that range
|
64
|
+
# note that it's inclusive
|
65
|
+
# if the caller says '2 .. 4', 4 should be a possibility
|
66
|
+
limit = unitize(@criteria.rand_inclusive)
|
67
|
+
lambda {|count| count < limit }
|
68
|
+
elsif @criteria.integer?
|
69
|
+
limit = unitize(@criteria)
|
70
|
+
lambda {|count| count < limit }
|
71
|
+
else
|
72
|
+
# never will succeed
|
73
|
+
FALSE
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def invoke(*args, &given_block)
|
78
|
+
# take in a block, or use the internally held one
|
79
|
+
# allows for external buffering
|
80
|
+
b = block_given? ? given_block : self.block
|
81
|
+
p = args
|
82
|
+
|
83
|
+
if b.arity < 1
|
84
|
+
# how interesting! arity of -1
|
85
|
+
b.call
|
86
|
+
else
|
87
|
+
# only as many args as supported
|
88
|
+
# do not worry about shortfall; that'll be a failure
|
89
|
+
p.pop while b.arity < p.size
|
90
|
+
b.call *p
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
|
95
|
+
|
96
|
+
def to_i(context)
|
97
|
+
value = nil
|
98
|
+
|
99
|
+
if Proc === @criteria
|
100
|
+
value = MadderLib::Context.invoke(@criteria, context)
|
101
|
+
elsif Range === @criteria
|
102
|
+
value = @criteria.max
|
103
|
+
elsif @criteria.integer?
|
104
|
+
value = @criteria
|
105
|
+
end
|
106
|
+
|
107
|
+
# has to be an integer, by definition
|
108
|
+
if value && value.respond_to?(:integer?) && value.integer?
|
109
|
+
unitize(value)
|
110
|
+
else
|
111
|
+
nil
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
|
116
|
+
|
117
|
+
# - - - - -
|
118
|
+
protected
|
119
|
+
|
120
|
+
def unitize(limit)
|
121
|
+
case @units
|
122
|
+
when :minutes, :minute
|
123
|
+
# no diff, for now
|
124
|
+
limit
|
125
|
+
when :times, :time
|
126
|
+
limit
|
127
|
+
else
|
128
|
+
limit
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
module MadderLib
|
2
|
+
module Conditional
|
3
|
+
#= Likely
|
4
|
+
#
|
5
|
+
#Introduces support for proportional selection from multiple Instructions in a given Phrase
|
6
|
+
module Likely
|
7
|
+
#The default weight for an Instruction, which is 1
|
8
|
+
DEFAULT_WEIGHT = 1
|
9
|
+
|
10
|
+
#= Likely::Phrase
|
11
|
+
#
|
12
|
+
#Introduces support for proportional selection from multiple Instructions in a given Phrase
|
13
|
+
#
|
14
|
+
#This is a very fancy way of saying 'weighted choices'.
|
15
|
+
#Using Phrase#alternately, multiple Instructions can be added to the same Phrase.
|
16
|
+
#Each one will either use the DEFAULT_WEIGHT, if not otherwise specified, or:
|
17
|
+
#
|
18
|
+
#* a numeric value
|
19
|
+
#* a Proc / lambda / block / closure which returns a numeric value
|
20
|
+
#
|
21
|
+
#The weights for all Instructions are totalled prior to each execution of the Builder.
|
22
|
+
#A random weight is chosen, and that defines the Instruction to be used
|
23
|
+
#
|
24
|
+
#See: Likely::Instruction
|
25
|
+
module Phrase
|
26
|
+
def self.included(target) #:nodoc:
|
27
|
+
# before each run, we need to prepare ourself
|
28
|
+
target.add_prepare do |phrase, context|
|
29
|
+
# no point in likelihood when there's only one choice
|
30
|
+
unless phrase.instructions.size < 2
|
31
|
+
weights = []
|
32
|
+
phrase.instructions.each do |instruction|
|
33
|
+
# put on a default weight if no other option
|
34
|
+
while (tester = instruction.conditional_likely_tester).nil?
|
35
|
+
instruction.likely(DEFAULT_WEIGHT)
|
36
|
+
end
|
37
|
+
|
38
|
+
weight = tester.to_i(context)
|
39
|
+
raise Error, 'invalid weight for instruction : #{instruction.words}' unless weight
|
40
|
+
weights << weight
|
41
|
+
end
|
42
|
+
|
43
|
+
# easy distributions
|
44
|
+
total = 0
|
45
|
+
Range.new(0, weights.size - 1).each do |index|
|
46
|
+
weight, instruction = weights[index], phrase.instructions[index]
|
47
|
+
|
48
|
+
state = context.state(instruction)
|
49
|
+
state[:likely_lower] = total
|
50
|
+
state[:likely_upper] = (total += weight)
|
51
|
+
end
|
52
|
+
|
53
|
+
# choose a random value
|
54
|
+
state = context.state(phrase)
|
55
|
+
state[:likely_total] = total
|
56
|
+
state[:likely_count] = rand(total) # Range.new(0, total).rand_inclusive
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
|
63
|
+
#Adds proportional logic to the current Instruction
|
64
|
+
#
|
65
|
+
#See: Instruction#likely
|
66
|
+
def likely(*args, &block)
|
67
|
+
self.instruction.likely *args, &block
|
68
|
+
end
|
69
|
+
alias :weight :likely
|
70
|
+
alias :weighted :likely
|
71
|
+
alias :weighing :likely
|
72
|
+
alias :odds :likely
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
|
77
|
+
#= Likely::Instruction
|
78
|
+
#
|
79
|
+
#Introduces support for proportional selection from multiple Instructions in a given Phrase
|
80
|
+
#
|
81
|
+
#See: Likely::Phrase
|
82
|
+
module Instruction
|
83
|
+
def self.included(target) #:nodoc:
|
84
|
+
# register a test to test all allowances for the instruction
|
85
|
+
# return false at the first one that fails
|
86
|
+
target.add_test do |instruction, context|
|
87
|
+
phrase = instruction.phrase
|
88
|
+
test = true
|
89
|
+
|
90
|
+
state = context.state(phrase)
|
91
|
+
total = state[:likely_total]
|
92
|
+
count = state[:likely_count]
|
93
|
+
|
94
|
+
# will only have a count if there's likelihood calc required
|
95
|
+
if count
|
96
|
+
state = context.state(instruction)
|
97
|
+
lower = state[:likely_lower]
|
98
|
+
upper = state[:likely_upper]
|
99
|
+
|
100
|
+
test = (count >= lower) && (count < upper)
|
101
|
+
|
102
|
+
if test && phrase.respond_to?(:recurs?) && phrase.recurs?
|
103
|
+
# set it up for the next recurrance
|
104
|
+
# we don't want the same thing over and over again
|
105
|
+
state[:likely_count] = rand(total)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
test
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
|
114
|
+
|
115
|
+
#Specifies the likelihood of this Instruction being used, compared to its siblings in their Phrase
|
116
|
+
#
|
117
|
+
#If provided, the arguments should contain:
|
118
|
+
#* a numeric value, which becomes the weight
|
119
|
+
#* a Range, or two numerics (which define a Range), from which the weight is chosen randomly
|
120
|
+
#* a Proc / lambda / block / closure, which returns a numeric value. \
|
121
|
+
#The block can either take no arguments, or a Context.
|
122
|
+
#
|
123
|
+
#See: Instruction#alternately
|
124
|
+
#
|
125
|
+
#Examples:
|
126
|
+
# builder = madderlib do
|
127
|
+
# say('parsley').likely(4)
|
128
|
+
# alternately(3).say('sage')
|
129
|
+
# alternately.say('rosemary').weighted(2).or.say('thyme')
|
130
|
+
# end
|
131
|
+
#
|
132
|
+
# usage = {}
|
133
|
+
# 60.times do
|
134
|
+
# key = builder.sentence
|
135
|
+
# usage[key] = (usage[key] || 0) + 1
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# # if proportions were accurately reproducible:
|
139
|
+
# # usage['parsley'].should eql(20)
|
140
|
+
# # usage['sage'].should eql(15)
|
141
|
+
# # usage['rosemary'].should eql(10)
|
142
|
+
# # usage['thyme'].should eql(5)
|
143
|
+
def likely(*args, &block)
|
144
|
+
# build a tester, set it aside
|
145
|
+
@likely_tester = Helper::TestBlock.new *args, &block
|
146
|
+
self
|
147
|
+
end
|
148
|
+
alias :weight :likely
|
149
|
+
alias :weighted :likely
|
150
|
+
alias :weighing :likely
|
151
|
+
alias :odds :likely
|
152
|
+
|
153
|
+
|
154
|
+
|
155
|
+
def conditional_likely_tester #:nodoc:
|
156
|
+
@likely_tester
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module MadderLib
|
2
|
+
module Conditional
|
3
|
+
#= Recur
|
4
|
+
#
|
5
|
+
#Introduces support for recurrant usage of a Phrase
|
6
|
+
module Recur
|
7
|
+
|
8
|
+
#= Recur::Phrase
|
9
|
+
#
|
10
|
+
#Introduces support for recurrant usage of a Phrase.
|
11
|
+
#This is particularly useful for an AnywherePhrase, whose position is randomly chosen.
|
12
|
+
#If it has a recurrence, it may appear multiple times.
|
13
|
+
#All of this is independent of whatever Instructions are contained within the Phrase
|
14
|
+
module Phrase
|
15
|
+
def self.included(target) #:nodoc:
|
16
|
+
# before each run, we need to prepare ourself
|
17
|
+
target.add_prepare do |phrase, context|
|
18
|
+
unless phrase.conditional_recur_tester
|
19
|
+
# we'll only run once if not told otherwise
|
20
|
+
phrase.recur 1
|
21
|
+
end
|
22
|
+
|
23
|
+
# we must retain state (the number of times called)
|
24
|
+
# and a consistent block for testing
|
25
|
+
state = context.state(phrase)
|
26
|
+
state[:recur_block] = phrase.conditional_recur_tester.block
|
27
|
+
state[:recur_count] = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
# register a test for recurrance
|
31
|
+
target.add_test do |phrase, context|
|
32
|
+
# where we at now
|
33
|
+
state = context.state(phrase)
|
34
|
+
block = state[:recur_block]
|
35
|
+
count = state[:recur_count]
|
36
|
+
state[:recur_count] = count + 1
|
37
|
+
|
38
|
+
# the block call returns the result of our test
|
39
|
+
# we buffered the block, but want to invoke it conveniently
|
40
|
+
phrase.conditional_recur_tester.invoke(count, context, &block)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
|
46
|
+
#Specifies the recurrance of this Phrase
|
47
|
+
#
|
48
|
+
#If provided, the arguments should contain:
|
49
|
+
#* a numeric value, which becomes the count
|
50
|
+
#* a Range, or two numerics (which define a Range), from which the count is chosen randomly
|
51
|
+
#* a Proc / lambda / block / closure, which returns false when the recurrance should stop. \
|
52
|
+
#The block can either take (a) no arguments, (b) the recurrance count, or; (c) the count <i>and</i> a Context.
|
53
|
+
#
|
54
|
+
#A recurrance count of 0 will exclude the Phrase from the Builder result
|
55
|
+
#
|
56
|
+
#A recurrance always ends when any Instruction returns an empty set of words.
|
57
|
+
#Processing will skip to the next Phrase, even if more recurrances are available
|
58
|
+
#
|
59
|
+
#Examples:
|
60
|
+
# builder = madderlib do
|
61
|
+
# say(:start)
|
62
|
+
# say(:end)
|
63
|
+
# anytime.recurring(2).say(:any)
|
64
|
+
# anytime.recurring {|count| count < 2 }.say(:also)
|
65
|
+
# end
|
66
|
+
#
|
67
|
+
# words = builder.words
|
68
|
+
# words.find_all {|word| word == 'any' }.should have(2).items
|
69
|
+
# words.find_all {|word| word == 'also' }.should have(2).items
|
70
|
+
def recur(*args, &block)
|
71
|
+
# build a tester, set it aside
|
72
|
+
@recur_tester = Helper::TestBlock.new *args, &block
|
73
|
+
self
|
74
|
+
end
|
75
|
+
alias :recurs :recur
|
76
|
+
alias :recurring :recur
|
77
|
+
|
78
|
+
#Returns true if the Phrase can recur.
|
79
|
+
#By default, the recurrance count is 1, in which case this method returns false
|
80
|
+
def recurs?(context=MadderLib::Context::EMPTY)
|
81
|
+
!! (conditional_recur_tester && (conditional_recur_tester.to_i(context) > 1))
|
82
|
+
end
|
83
|
+
|
84
|
+
|
85
|
+
|
86
|
+
def conditional_recur_tester #:nodoc:
|
87
|
+
@recur_tester
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
|
92
|
+
|
93
|
+
module Instruction #:nodoc:
|
94
|
+
# not impacted
|
95
|
+
# the Phrase repeats, not the Instruction
|
96
|
+
# we could proxy the recur back to the active phrase
|
97
|
+
# but that would be confusing
|
98
|
+
# you should only set up recurrence once; it's singleton to the phrase
|
99
|
+
end
|
100
|
+
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|