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.
Files changed (40) hide show
  1. data/CHANGELOG +3 -0
  2. data/LICENSE +22 -0
  3. data/README.rdoc +173 -0
  4. data/Rakefile +134 -0
  5. data/lib/madderlib.rb +27 -0
  6. data/lib/madderlib/builder.rb +659 -0
  7. data/lib/madderlib/conditional/allowed.rb +144 -0
  8. data/lib/madderlib/conditional/helper.rb +135 -0
  9. data/lib/madderlib/conditional/likely.rb +162 -0
  10. data/lib/madderlib/conditional/recur.rb +103 -0
  11. data/lib/madderlib/conditional/registry.rb +56 -0
  12. data/lib/madderlib/conditional/repeat.rb +130 -0
  13. data/lib/madderlib/context.rb +140 -0
  14. data/lib/madderlib/core.rb +217 -0
  15. data/lib/madderlib/extensions.rb +40 -0
  16. data/lib/madderlib/instruction.rb +171 -0
  17. data/lib/madderlib/phrase.rb +284 -0
  18. data/lib/madderlib/sequencer.rb +337 -0
  19. data/madderlib.gemspec +72 -0
  20. data/spec/benchmark_spec.rb +69 -0
  21. data/spec/builder_spec.rb +321 -0
  22. data/spec/builder_to_other_spec.rb +47 -0
  23. data/spec/builder_to_sequencer_spec.rb +388 -0
  24. data/spec/conditional_allowed_spec.rb +130 -0
  25. data/spec/conditional_helper_spec.rb +131 -0
  26. data/spec/conditional_likely_spec.rb +138 -0
  27. data/spec/conditional_recur_spec.rb +102 -0
  28. data/spec/conditional_registry_spec.rb +94 -0
  29. data/spec/conditional_repeat_spec.rb +88 -0
  30. data/spec/doc_spec.rb +550 -0
  31. data/spec/error_spec.rb +33 -0
  32. data/spec/examples_spec.rb +151 -0
  33. data/spec/extensions_spec.rb +47 -0
  34. data/spec/grammar_spec.rb +101 -0
  35. data/spec/instruction_spec.rb +133 -0
  36. data/spec/kernel_spec.rb +58 -0
  37. data/spec/phrase_spec.rb +7 -0
  38. data/spec/sequencer_spec.rb +317 -0
  39. data/spec/spec_helper.rb +54 -0
  40. 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