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,40 @@
|
|
1
|
+
class Range
|
2
|
+
#Returns a random number between the min and max of the Range.
|
3
|
+
#An optional precision can be provided, which assumed to be 0 (eg. Fixnum.floor)
|
4
|
+
def rand(precision=0)
|
5
|
+
rand_from self.min, self.max, precision
|
6
|
+
end
|
7
|
+
|
8
|
+
#Returns a random number within the min and max of the Range, which can potentially include the max.
|
9
|
+
#An optional precision can be provided, which assumed to be 0 (eg. Fixnum.floor)
|
10
|
+
def rand_inclusive(precision=0)
|
11
|
+
rand_from self.min, self.max + 1, precision
|
12
|
+
end
|
13
|
+
|
14
|
+
# - - - - -
|
15
|
+
private
|
16
|
+
|
17
|
+
def rand_from(a, b, precision)
|
18
|
+
span = [a, b]
|
19
|
+
min, max = span.min, span.max
|
20
|
+
|
21
|
+
min + if precision == 0
|
22
|
+
# no effort required
|
23
|
+
Kernel.rand(max - min).floor
|
24
|
+
else
|
25
|
+
# some precision
|
26
|
+
p = (10 ** precision).to_f
|
27
|
+
Kernel.rand((max - min) * p) / p
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
|
34
|
+
class Array
|
35
|
+
#Composites the Array -- of Fixnums (characters) -- into a String.
|
36
|
+
#An exception will be raised if any value in the Array is not a Fixnum
|
37
|
+
def to_byte_s
|
38
|
+
self.collect {|c| c.chr }.join(nil)
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,171 @@
|
|
1
|
+
module MadderLib
|
2
|
+
#= Instruction
|
3
|
+
#
|
4
|
+
#A specific instruction within a MadderLib ruleset.
|
5
|
+
#
|
6
|
+
#A Phrase is comprised of one or more Instructions.
|
7
|
+
#Each Instruction contains the logic necessary to produce the result for the Phrase
|
8
|
+
#
|
9
|
+
#Typical 'words' that can appear in an Instruction are:
|
10
|
+
#* a String
|
11
|
+
#* a Symbol
|
12
|
+
#* a Proc / lambda / block / closure
|
13
|
+
#* another Builder
|
14
|
+
#* an Array of the above
|
15
|
+
#
|
16
|
+
#The logic by which these Object types are resolved is described within wordify .
|
17
|
+
#
|
18
|
+
#An Instruction also supports:
|
19
|
+
#
|
20
|
+
#* repetition, via Conditional::Repeat
|
21
|
+
#* conditional usage, via Conditional::Allowed
|
22
|
+
#* proportionate usage, via Conditional::Likely
|
23
|
+
class Instruction
|
24
|
+
class << self
|
25
|
+
include Conditional::Registry::Static
|
26
|
+
end
|
27
|
+
include Conditional::Registry::Instance
|
28
|
+
|
29
|
+
#A refererence to the Phrase which contains this Instruction
|
30
|
+
attr_reader :phrase
|
31
|
+
#An Array of the words produced by this Instruction.
|
32
|
+
#The term 'word' is used very loosely here; they are simply the Objects provided during construction
|
33
|
+
attr_reader :words
|
34
|
+
|
35
|
+
|
36
|
+
|
37
|
+
#Constructs a new Instruction
|
38
|
+
#
|
39
|
+
#The containing Phrase is required.
|
40
|
+
#
|
41
|
+
#Any number of arguments may be provided, and they become the Instruction's words.
|
42
|
+
#An optional block can be provided, and if so, it is also treated as a word (since a Proc is considered a valid 'word')
|
43
|
+
def initialize(phrase, *args, &block)
|
44
|
+
@phrase = phrase
|
45
|
+
@words = args
|
46
|
+
@words << block if block_given?
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
#A proxy for phrase.alternately .
|
52
|
+
#The Phrase#alternately method is invoked against the Instruction's containing Phrase with the arguments provided
|
53
|
+
def alternately(*args, &block)
|
54
|
+
# proxy to the phrase
|
55
|
+
self.phrase.or *args, &block
|
56
|
+
end
|
57
|
+
alias :or :alternately
|
58
|
+
|
59
|
+
|
60
|
+
|
61
|
+
#Generates the list of words for this Instruction.
|
62
|
+
#
|
63
|
+
#This method returns a flattened Array of all the Instruction's words, resolved as of 'now'.
|
64
|
+
#All blank and nil values are removed from the Array
|
65
|
+
#
|
66
|
+
#A thorough description of how words are resolved can be find in the wordify method
|
67
|
+
#
|
68
|
+
#Example:
|
69
|
+
# builder = madderlib do
|
70
|
+
# say nil
|
71
|
+
# say ''
|
72
|
+
# end
|
73
|
+
# builder.words.should eql([])
|
74
|
+
#
|
75
|
+
# builder = madderlib do
|
76
|
+
# say 'one'
|
77
|
+
# say :two
|
78
|
+
# say 3
|
79
|
+
# end
|
80
|
+
# builder.words.should eql(%w{ one two 3 })
|
81
|
+
#
|
82
|
+
# builder = madderlib do
|
83
|
+
# say []
|
84
|
+
# say [ nil, 'one' ]
|
85
|
+
# say [ :two, [ '', 3 ]]
|
86
|
+
# end
|
87
|
+
# builder.words.should eql(%w{ one two 3 })
|
88
|
+
#
|
89
|
+
# builder = madderlib do
|
90
|
+
# say madderlib { say 'one' }
|
91
|
+
# say madderlib {
|
92
|
+
# say madderlib { say :two }
|
93
|
+
# say madderlib { say 3 }
|
94
|
+
# }
|
95
|
+
# end
|
96
|
+
# builder.words.should eql(%w{ one two 3 })
|
97
|
+
#
|
98
|
+
# words = [ 'one', lambda { :two }, madderlib { say 3 } ]
|
99
|
+
# builder = madderlib do
|
100
|
+
# say { words.shift }.repeat { ! words.empty? }
|
101
|
+
# end
|
102
|
+
# builder.words.should eql(%w{ one two 3 })
|
103
|
+
def speak(context)
|
104
|
+
# wordify everything, and strip out blanks & nils
|
105
|
+
spoken = self.class.wordify(words, context)
|
106
|
+
spoken.find_all {|word| word && (! word.empty?) }
|
107
|
+
end
|
108
|
+
|
109
|
+
|
110
|
+
|
111
|
+
include Conditional::Allowed::Instruction
|
112
|
+
include Conditional::Repeat::Instruction
|
113
|
+
include Conditional::Likely::Instruction
|
114
|
+
|
115
|
+
# - - - - -
|
116
|
+
protected
|
117
|
+
|
118
|
+
class << self
|
119
|
+
#Converts the object passed into a 'word', according to the following rules:
|
120
|
+
#
|
121
|
+
#* <code>nil</code> => nil
|
122
|
+
#* <code>String</code> => itself (blank Strings also)
|
123
|
+
#* <code>Proc / lambda / block / closure</code> => the result of invoking the block. \
|
124
|
+
#The block can either take no arguments, or a Context. \
|
125
|
+
#Please note that resolution of the Proc's scoped variables (etc.) occurs only at the time that wordify is invoked!
|
126
|
+
#* <code>Builder</code> => the result of Builder#words, which will be an Array
|
127
|
+
#* <code>Array</code> => a flattened Array of the each element, as converted via wordify
|
128
|
+
#
|
129
|
+
#Anything other Object type is converted to a string via Object#to_s.
|
130
|
+
#
|
131
|
+
#See: speak
|
132
|
+
def wordify(source, context)
|
133
|
+
# our own dogfood
|
134
|
+
if Builder === source
|
135
|
+
# build the words
|
136
|
+
# pull back and track the context
|
137
|
+
# we know they'll be Strings, so no need for recursion
|
138
|
+
return source.words {|sub_context| context.add_context sub_context }
|
139
|
+
end
|
140
|
+
|
141
|
+
while (Proc === source)
|
142
|
+
# evaluate, then wordify the result
|
143
|
+
# this addresses the Proc-returns-a-Proc condition
|
144
|
+
# it's a corner case
|
145
|
+
source = Context.invoke(source, context)
|
146
|
+
end
|
147
|
+
|
148
|
+
if (Array === source)
|
149
|
+
# recursive parsing
|
150
|
+
# plus flattening!
|
151
|
+
source = source.inject([]) do |a, word|
|
152
|
+
word = wordify(word, context)
|
153
|
+
|
154
|
+
if Array === word
|
155
|
+
a += word
|
156
|
+
else
|
157
|
+
a << word
|
158
|
+
end
|
159
|
+
|
160
|
+
a
|
161
|
+
end
|
162
|
+
elsif source && ! (String === source)
|
163
|
+
# don't stringify nil
|
164
|
+
source = source.to_s
|
165
|
+
end
|
166
|
+
|
167
|
+
source
|
168
|
+
end
|
169
|
+
end
|
170
|
+
end
|
171
|
+
end
|
@@ -0,0 +1,284 @@
|
|
1
|
+
module MadderLib
|
2
|
+
#= Phrase
|
3
|
+
#
|
4
|
+
#A specific phrase within a MadderLib Builder.
|
5
|
+
#
|
6
|
+
#A Phrase is a collection of one or more Instruction objects.
|
7
|
+
#An Instruction is selected, and that becomes the Phrase's result
|
8
|
+
#
|
9
|
+
#A Phrase supports:
|
10
|
+
#
|
11
|
+
#* proportioned choice of Instructions, via Conditional::Likely
|
12
|
+
class Phrase
|
13
|
+
class << self
|
14
|
+
include Conditional::Registry::Static
|
15
|
+
end
|
16
|
+
include Conditional::Registry::Instance
|
17
|
+
|
18
|
+
#A refererence to the Builder which contains this Phrase
|
19
|
+
attr_reader :builder
|
20
|
+
#The (optional) id of the Phrase
|
21
|
+
attr_reader :id
|
22
|
+
#An Array of the Instructions used by this Phrase
|
23
|
+
attr_reader :instructions
|
24
|
+
|
25
|
+
|
26
|
+
|
27
|
+
#Constructs a new Phrase
|
28
|
+
#
|
29
|
+
#The containing Phrase is builder.
|
30
|
+
#
|
31
|
+
#An optional id can be provided.
|
32
|
+
#The id is particularly useful in the case of:
|
33
|
+
#* relative positioning, via Builder#before or Builder#after
|
34
|
+
#* conditional usage, via Conditional::Allowed#assuming , etc.
|
35
|
+
#* positioning ranges, via AnywherePhrase#between , etc.
|
36
|
+
#
|
37
|
+
#Any number of arguments may be provided, and they are dispatched to the say method.
|
38
|
+
#An optional block can be provided, and if so, it is also dispatched to say.
|
39
|
+
def initialize(builder, id=nil, *args, &block)
|
40
|
+
@builder, @id = builder, id
|
41
|
+
@instructions = []
|
42
|
+
|
43
|
+
# don't start out with an empty instruction
|
44
|
+
say *args, &block unless (args.empty?) && (! block_given?)
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
|
49
|
+
#Adds a new Instruction to the Phrase using the provided arguments.
|
50
|
+
#
|
51
|
+
#All arguments, and any block provided, are used to construct the new Instruction.
|
52
|
+
#Any proportion logic which has been cached via a prior call to alternately us applied to the new Instruction
|
53
|
+
#
|
54
|
+
#See: Builder#say
|
55
|
+
def say(*args, &block)
|
56
|
+
# allocate new
|
57
|
+
@instructions << Instruction.new(self, *args, &block)
|
58
|
+
|
59
|
+
if @or_likely
|
60
|
+
# retro-apply the likelihood from the 'or' operation
|
61
|
+
args, block = @or_likely
|
62
|
+
self.instruction.likely *args, &block unless (args.empty? && block.nil?)
|
63
|
+
@or_likely = nil
|
64
|
+
end
|
65
|
+
|
66
|
+
self
|
67
|
+
end
|
68
|
+
alias :says :say
|
69
|
+
|
70
|
+
#Sets aside proportion logic to be used by the next call to the say method.
|
71
|
+
#
|
72
|
+
#Calling this method is a prelude to adding another optional Instruction via say.
|
73
|
+
#All arguments are optional; if none are provided, the default proportions will be assumed.
|
74
|
+
#Without any arguments, a call to this method is syntactic sugar.
|
75
|
+
#You could call say(...).say(...), but it's not quite as descriptive
|
76
|
+
#
|
77
|
+
#See: Builder#alternately
|
78
|
+
#
|
79
|
+
#Examples:
|
80
|
+
# builder = madderlib do
|
81
|
+
# say('barnard').or.say('bryn mawr')
|
82
|
+
# alternately(2).say('mount holyoke').alternately(2).say('radcliffe')
|
83
|
+
# it.alternately(4).says('smith').or(4).says('vassar')
|
84
|
+
# end
|
85
|
+
# builder.phrase.says('wellesley').or(5).nothing
|
86
|
+
#
|
87
|
+
# usage = {}
|
88
|
+
# 200.times do
|
89
|
+
# key = builder.sentence
|
90
|
+
# usage[key] = (usage[key] || 0) + 1
|
91
|
+
# end
|
92
|
+
#
|
93
|
+
# # if proportions were accurately reproducible:
|
94
|
+
# # ['barnard', 'bryn mawr', 'wellesley'].each {|name| usage[name].should eql(10) }
|
95
|
+
# # ['mount holyoke', 'radcliffe'].each {|name| usage[name].should eql(20) }
|
96
|
+
# # ['smith', 'vassar'].each {|name| usage[name].should eql(40) }
|
97
|
+
# # [''].each {|name| usage[name].should eql(50) }
|
98
|
+
def alternately(*args, &block)
|
99
|
+
# hold onto these until we say something
|
100
|
+
@or_likely = [args, block]
|
101
|
+
self
|
102
|
+
end
|
103
|
+
alias :or :alternately
|
104
|
+
|
105
|
+
#Adds a new Instruction to the Phrase which has no content.
|
106
|
+
#The Instruction will always return an empty Array, and thus be omitted from the Builder results
|
107
|
+
#
|
108
|
+
#This method is provided for proportionate Phrases, where 'nothing' may be a suitable output
|
109
|
+
#
|
110
|
+
#See: alternately
|
111
|
+
def nothing
|
112
|
+
# say nothing
|
113
|
+
say
|
114
|
+
end
|
115
|
+
|
116
|
+
|
117
|
+
|
118
|
+
#Returns the current Instruction.
|
119
|
+
#
|
120
|
+
#An Error will be raised if there is no current Instruction.
|
121
|
+
#It's an easy condition to recover from, but it's bad use of the syntax.
|
122
|
+
def instruction
|
123
|
+
raise Error, 'there is no current Instruction' if @instructions.empty?
|
124
|
+
|
125
|
+
# whatever our current once is
|
126
|
+
@instructions.last
|
127
|
+
end
|
128
|
+
|
129
|
+
|
130
|
+
|
131
|
+
#Generates the list of words for this Phrase
|
132
|
+
#
|
133
|
+
#This is done by:
|
134
|
+
#* choosing a suitable Instruction
|
135
|
+
#* invoking Instruction#speak
|
136
|
+
#
|
137
|
+
#See: Instruction#speak
|
138
|
+
def speak(context)
|
139
|
+
found = nil
|
140
|
+
|
141
|
+
# should we speak at all?
|
142
|
+
if self.test(context)
|
143
|
+
# say the first sensible thing
|
144
|
+
found = instructions.find do |instruction|
|
145
|
+
instruction.test(context)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
if found
|
150
|
+
# track our instructions
|
151
|
+
context.instructions << found
|
152
|
+
|
153
|
+
# now, speak your say (as words)
|
154
|
+
found = found.speak(context)
|
155
|
+
end
|
156
|
+
found || []
|
157
|
+
end
|
158
|
+
|
159
|
+
|
160
|
+
|
161
|
+
include Conditional::Allowed::Phrase
|
162
|
+
include Conditional::Repeat::Phrase
|
163
|
+
include Conditional::Likely::Phrase
|
164
|
+
end
|
165
|
+
|
166
|
+
|
167
|
+
|
168
|
+
#= AnytimePhrase
|
169
|
+
#
|
170
|
+
#A Phrase constructed by Builder#anywhere
|
171
|
+
#
|
172
|
+
#Beyond what a standard Phrase can do, an AnytimePhrase can specify:
|
173
|
+
#
|
174
|
+
#* the range of positions where it can be inserted into the Builder result
|
175
|
+
#* recurring usage, via Conditional::Recur .\
|
176
|
+
#This is <i>not</i> the same as having a repeating Instruction. \
|
177
|
+
#The Phrase recurrance indicates how many times it is resolved and inserted into the Builder result. \
|
178
|
+
#For example, a recurring Phrase can contain a repeating Instruction.
|
179
|
+
class AnytimePhrase < Phrase
|
180
|
+
def initialize(*args)
|
181
|
+
super
|
182
|
+
end
|
183
|
+
|
184
|
+
|
185
|
+
|
186
|
+
#States that this Phrase should only appear before the referenced Phrase, by id
|
187
|
+
#
|
188
|
+
#If the referenced Phrase does not appear in the Builder result, it can appear anywhere
|
189
|
+
#
|
190
|
+
#Examples:
|
191
|
+
# flag = true
|
192
|
+
# builder = madderlib do
|
193
|
+
# say 'top'
|
194
|
+
# also(:limit).say('middle').if { flag }
|
195
|
+
# say 'bottom'
|
196
|
+
#
|
197
|
+
# anywhere.say('hello').before(:limit)
|
198
|
+
# end
|
199
|
+
#
|
200
|
+
# 10.times do
|
201
|
+
# words = builder.words
|
202
|
+
# words.index('hello').should eql(1)
|
203
|
+
# end
|
204
|
+
#
|
205
|
+
# flag = false
|
206
|
+
# 10.times do
|
207
|
+
# words = builder.words
|
208
|
+
# (words.index('hello') < 2).should be_true
|
209
|
+
# end
|
210
|
+
def before(ref=nil)
|
211
|
+
if ref
|
212
|
+
# settter
|
213
|
+
@before = ref
|
214
|
+
self
|
215
|
+
else
|
216
|
+
# getter
|
217
|
+
@before
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
#States that this Phrase should only appear before the referenced Phrase, by id
|
222
|
+
#
|
223
|
+
#If the referenced Phrase does not appear in the Builder result, it can appear anywhere
|
224
|
+
#
|
225
|
+
#Examples:
|
226
|
+
# flag = true
|
227
|
+
# builder = madderlib do
|
228
|
+
# say 'top'
|
229
|
+
# also(:limit).say('middle').if { flag }
|
230
|
+
# say 'bottom'
|
231
|
+
#
|
232
|
+
# anywhere.say('hello').after(:limit)
|
233
|
+
# end
|
234
|
+
#
|
235
|
+
# 10.times do
|
236
|
+
# words = builder.words
|
237
|
+
# words.index('hello').should eql(2)
|
238
|
+
# end
|
239
|
+
#
|
240
|
+
# flag = false
|
241
|
+
# 10.times do
|
242
|
+
# words = builder.words
|
243
|
+
# (words.index('hello') > 0).should be_true
|
244
|
+
# end
|
245
|
+
def after(ref=nil)
|
246
|
+
if ref
|
247
|
+
# settter
|
248
|
+
@after = ref
|
249
|
+
self
|
250
|
+
else
|
251
|
+
# getter
|
252
|
+
@after
|
253
|
+
end
|
254
|
+
end
|
255
|
+
|
256
|
+
#A shorthand for expression both after and before limits
|
257
|
+
#
|
258
|
+
#The first argument is for after, the second is for before
|
259
|
+
#
|
260
|
+
#Examples:
|
261
|
+
# builder = madderlib do
|
262
|
+
# say 'top'
|
263
|
+
# also(:upper).say('upper')
|
264
|
+
# also(:lower).say('lower')
|
265
|
+
# say 'bottom'
|
266
|
+
#
|
267
|
+
# anywhere.say('hello').between(:upper, :lower)
|
268
|
+
# end
|
269
|
+
#
|
270
|
+
# 10.times do
|
271
|
+
# words = builder.words
|
272
|
+
# words.index('hello').should eql(2)
|
273
|
+
# end
|
274
|
+
def between(a, b)
|
275
|
+
after a
|
276
|
+
before b
|
277
|
+
self
|
278
|
+
end
|
279
|
+
|
280
|
+
|
281
|
+
|
282
|
+
include Conditional::Recur::Phrase
|
283
|
+
end
|
284
|
+
end
|