madderlib 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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,337 @@
1
+ module MadderLib
2
+ #= Sequencer
3
+ #
4
+ #A utility class used by a Builder for the actual building process.
5
+ #
6
+ #A Sequencer provides no useful functionality to users of MadderLib.
7
+ #All of its value is inherent in the Builder and Phrase objects that it leverages.
8
+ class Sequencer #:nodoc: all
9
+ include Enumerable
10
+
11
+ attr_reader :builder, :steps, :ids
12
+
13
+ def initialize(builder, steps, ids, attrs={})
14
+ @builder, @steps, @ids = builder, steps, ids
15
+
16
+ # arbitrary attributes for convenience
17
+ (attrs || {}).each do |k, v|
18
+ self.instance_variable_set "@#{k}".to_sym, v
19
+ end
20
+
21
+ # prime
22
+ @anytime ||= []
23
+ @before ||= {}
24
+ @after ||= {}
25
+ end
26
+
27
+ def before(ref)
28
+ @before[ref]
29
+ end
30
+
31
+ def after(ref)
32
+ @after[ref]
33
+ end
34
+
35
+
36
+
37
+ # returns each word in the sequence
38
+ def words(data=nil, &block)
39
+ # sequence the phrases
40
+ # *sigh* verb and a noun
41
+ nodes = sequence(data, &block)
42
+
43
+ # composite the words
44
+ # each node contains an array of words
45
+ nodes.collect {|node| node.words }.flatten
46
+ end
47
+
48
+ # iterates over each word in the sequence
49
+ def each_word(data=nil)
50
+ self.words(data).each {|word| yield word }
51
+ end
52
+ alias :each :each_word
53
+
54
+ # returns each phrase in the sequence
55
+ def phrases(data=nil, &block)
56
+ # sequence the phrases
57
+ # *sigh* verb and a noun
58
+ nodes = sequence(data, &block)
59
+
60
+ # only care about the phrases
61
+ # these will all have been 'spoken'
62
+ nodes.collect {|node| node.phrase }
63
+ end
64
+
65
+ # iterates over each phrase in the sequence
66
+ def each_phrase(data=nil, &block)
67
+ self.phrases(data).each {|phrase| yield phrase }
68
+ end
69
+
70
+
71
+
72
+ # - - - - -
73
+ protected
74
+
75
+ RESULT_NODE = Struct.new(:phrase, :words, :before, :after)
76
+
77
+ def sequence(data=nil, &block)
78
+ # this is where we do all the sequencing
79
+ # each phrase gets invoked, and any words it returns are used
80
+ # if no words are returned, it's skipped
81
+ # otherwise, look for befores and afters, and apply them
82
+ # same logic for each before / after as above
83
+ # then, pepper in the anytimes, including boundaries, etc
84
+
85
+ # fresh context
86
+ context = Context.new(self)
87
+
88
+ # bring in all the custom data
89
+ context.data.merge!(data) if data
90
+
91
+ # one-shot dispatch
92
+ Context.invoke(block, context) if block_given?
93
+
94
+ if (@setup)
95
+ # dispatch to each block
96
+ @setup.each {|blk| Context.invoke(blk, context) }
97
+ end
98
+
99
+ # all the basic steps
100
+ # one node per phrase
101
+ # in the proper order
102
+ # we can flatten it immediately
103
+ result_nodes = []
104
+ steps.each do |phrase|
105
+ # make sure it's prepared
106
+ phrase.prepare(context)
107
+
108
+ # accumuluate its wisdom
109
+ result_nodes += traverse(phrase, context)
110
+ end
111
+
112
+ # link them all together
113
+ # the Array and the nodes are in sequence
114
+ # this allows for anytime insertions without re-indexing
115
+ prev = nil
116
+ result_nodes.each do |node|
117
+ if prev
118
+ prev.after = node
119
+ node.before = prev
120
+ end
121
+ prev = node
122
+ end
123
+
124
+ # now pepper in the anytimes
125
+ result_size = result_nodes.size
126
+
127
+ # this will always find the first match
128
+ # TODO: optionally we could create two loops for before and after
129
+ index_of = lambda do |id|
130
+ found = nil
131
+ Range.new(0, result_size - 1).each do |idx|
132
+ if result_nodes[idx].phrase.id == id
133
+ # grr. can't return.
134
+ found = idx
135
+ break
136
+ end
137
+ end
138
+ found
139
+ end
140
+
141
+ @anytime.each do |anytime|
142
+ # make sure it's prepared
143
+ anytime.prepare(context)
144
+
145
+ loop do
146
+ # we don't want an anytime to end the sentence
147
+ # consistent with not letting one start, which is implicit here
148
+ # UNLESS it's an empty set of nodes
149
+ before_index = false
150
+
151
+ to = result_size - 1
152
+ if anytime.before
153
+ # expose this for subsequent checks
154
+ before_index = index_of.call(anytime.before)
155
+
156
+ # you can't intentionally inject before the first item
157
+ # it's not an error, it's just there's no room for anytime
158
+ if before_index
159
+ break if (before_index == 0)
160
+ to = before_index
161
+ end
162
+ end
163
+
164
+ # can't start the sentence either
165
+ # eg. can't insert before 0
166
+ from = 0
167
+ if anytime.after
168
+ index = index_of.call(anytime.after)
169
+
170
+ # you can't intentionally inject after the last item
171
+ # it's not an error, it's just there's no room for anytime
172
+ if index
173
+ break if (index == (result_size - 1))
174
+ from = index
175
+ end
176
+
177
+ if to < from
178
+ # you can't explicitly bound like that
179
+ raise Error, "bounding failure between #{anytime.after.inspect} and #{anytime.before.inspect}" if (index && before_index)
180
+
181
+ # partially bounded conditions, that's different ...
182
+ # handle special case: there's only one place to put the thing
183
+ from = to
184
+ end
185
+ end
186
+
187
+ # we'd get nothing if
188
+ # the anytime is exhauseted
189
+ # it conditionally returned nothing
190
+ # both of those are treated as exit conditions
191
+ # if you don'w want conditional nothings, don't permit them
192
+ these_nodes = traverse(anytime, context)
193
+ break if these_nodes.empty?
194
+
195
+ # TODO:
196
+ # add logic so that multiple anytimes won't be adjacent
197
+ # requires an exhausting index pool
198
+ # probably okay to stop once the pool is exhausted, even if there's more anytime
199
+ # if will get appended after the chosen position
200
+ # don't want to append to end of sequence, so (to -= 1)
201
+ # so offset
202
+ # it is possible to have only one place to put the thing
203
+ to -= 1
204
+ to = 0 if to < 0
205
+
206
+ where = ((from >= to) ? from : Range.new(from, to).rand)
207
+ node = result_nodes[where]
208
+
209
+ unless node
210
+ # special case: append to end
211
+ # see considerations above
212
+ raise Error, "no node found at #{where}" unless where == result_size
213
+ if where == 0
214
+ # you're all there is
215
+ result_nodes += these_nodes
216
+ else
217
+ # append to end
218
+ node = result_nodes.last
219
+ end
220
+ end
221
+
222
+ if node
223
+ # splice
224
+ head = these_nodes.first
225
+ tail = these_nodes.last
226
+
227
+ tail.after = node.after
228
+ tail.after.before = tail if tail.after
229
+ node.after = head
230
+ head.before = node
231
+ end
232
+ end
233
+ end
234
+
235
+ # finally, flatten the node tree
236
+ flattened = []
237
+ node = result_nodes.first
238
+ while (node)
239
+ flattened << node
240
+ node = node.after
241
+ end
242
+
243
+ if (@teardown)
244
+ # dispatch to each block
245
+ @teardown.each {|blk| Context.invoke(blk, context) }
246
+ end
247
+
248
+ flattened
249
+ end
250
+
251
+ def traverse(phrase, context)
252
+ evaluate = lambda do |phrz|
253
+ words = phrz.speak(context)
254
+ words = [words] unless Array === words
255
+
256
+ # track how our phrases are used
257
+ if words.empty?
258
+ context.silent << phrz
259
+ else
260
+ context.spoken << phrz
261
+ context.spoken_ids << phrz.id if phrz.id
262
+ end
263
+
264
+ words
265
+ end
266
+
267
+ result_nodes = []
268
+
269
+ # build the phrase hierarchy
270
+ # before / after tree, with non-recursive queue
271
+ # chose a queue to imply some order
272
+ tree = RESULT_NODE.new(phrase, nil, nil, nil)
273
+ queue = [tree]
274
+
275
+ while (node = queue.shift)
276
+ # evaluate the phrase
277
+ node.words = evaluate.call(node.phrase)
278
+ next if node.words.empty?
279
+
280
+ # check the wrappers
281
+ # each is an array of phrases
282
+ id = node.phrase.id
283
+
284
+ # as built, for understandability, the first before is sequentially first
285
+ # we're appending in reference to an un-moving node
286
+ # and it actually works the same way...
287
+ # the farther away the node, the earlier it will be traversed
288
+ (self.before(id) || []).each do |more|
289
+ node.before = RESULT_NODE.new(more, nil, node.before, nil)
290
+ queue << node.before
291
+ end
292
+
293
+ # the last after is sequentially last
294
+ # the node doesn't move, and the last after needs to be at the end of the chain
295
+ # so, reverse order is appropriate
296
+ # heh. this is not meant to be performant, it's meant to be comprehensible
297
+ # at least at the individual captured data structure level
298
+ (self.after(id) || []).reverse.each do |more|
299
+ node.after = RESULT_NODE.new(more, nil, nil, node.after)
300
+ queue << node.after
301
+ end
302
+ end
303
+
304
+ # traverse / flatten the tree
305
+ # while pull
306
+ # if children, re-push [after node before], null its children
307
+ # if no children, append
308
+ # result nodes are appended via stack ordering, so it's 'backwards'
309
+ # *YES* this is destructive
310
+
311
+ stack = [tree]
312
+
313
+ while (node = stack.pop)
314
+ # we could have empty nodes
315
+ # they won't have children, that was taken care of
316
+ # but the node placeholders will still be there
317
+ next if node.words.empty?
318
+
319
+ if (node.before || node.after)
320
+ # re-traverse us all
321
+ stack << node.after if node.after
322
+ stack << node
323
+ stack << node.before if node.before
324
+
325
+ # pretend i didn't have any children
326
+ node.before = node.after = nil
327
+ else
328
+ result_nodes << node
329
+ end
330
+ end
331
+
332
+ # the nodes we return are *not* linked together
333
+ # just an array
334
+ result_nodes
335
+ end
336
+ end
337
+ end
data/madderlib.gemspec ADDED
@@ -0,0 +1,72 @@
1
+ Gem::Specification.new do |s|
2
+ s.specification_version = 2 if s.respond_to? :specification_version=
3
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
4
+ s.required_ruby_version = '>= 1.8'
5
+
6
+ s.name = 'madderlib'
7
+ s.version = '0.1.0'
8
+ s.date = "2009-02-14"
9
+
10
+ s.description = "MadderLib : a Sentence-Building DSL for the easily amused"
11
+ s.summary = "#{s.name} #{s.version}"
12
+
13
+ s.homepage = "http://wiki.cantremember.com/MadderLib"
14
+ s.authors = ["Dan Foley"]
15
+ s.email = 'admin@cantremember.com' # = MANIFEST =
16
+ s.files = %w[
17
+ CHANGELOG
18
+ LICENSE
19
+ README.rdoc
20
+ Rakefile
21
+ lib/madderlib.rb
22
+ lib/madderlib/builder.rb
23
+ lib/madderlib/conditional/allowed.rb
24
+ lib/madderlib/conditional/helper.rb
25
+ lib/madderlib/conditional/likely.rb
26
+ lib/madderlib/conditional/recur.rb
27
+ lib/madderlib/conditional/registry.rb
28
+ lib/madderlib/conditional/repeat.rb
29
+ lib/madderlib/context.rb
30
+ lib/madderlib/core.rb
31
+ lib/madderlib/extensions.rb
32
+ lib/madderlib/instruction.rb
33
+ lib/madderlib/phrase.rb
34
+ lib/madderlib/sequencer.rb
35
+ madderlib.gemspec
36
+ spec/benchmark_spec.rb
37
+ spec/builder_spec.rb
38
+ spec/builder_to_other_spec.rb
39
+ spec/builder_to_sequencer_spec.rb
40
+ spec/conditional_allowed_spec.rb
41
+ spec/conditional_helper_spec.rb
42
+ spec/conditional_likely_spec.rb
43
+ spec/conditional_recur_spec.rb
44
+ spec/conditional_registry_spec.rb
45
+ spec/conditional_repeat_spec.rb
46
+ spec/doc_spec.rb
47
+ spec/error_spec.rb
48
+ spec/examples_spec.rb
49
+ spec/extensions_spec.rb
50
+ spec/grammar_spec.rb
51
+ spec/instruction_spec.rb
52
+ spec/kernel_spec.rb
53
+ spec/phrase_spec.rb
54
+ spec/sequencer_spec.rb
55
+ spec/spec_helper.rb
56
+ ]
57
+ # = MANIFEST =
58
+
59
+ s.require_paths = %w{lib}
60
+
61
+ =begin
62
+ s.add_dependency 'GEM-NAME', '>= GEM-VERSION'
63
+ =end
64
+
65
+ s.has_rdoc = true
66
+ # only because there ain't no spaces in the title ...
67
+ s.rdoc_options = %w{ --line-numbers --inline-source --title MadderLib --main README.rdoc }
68
+ s.extra_rdoc_files = %w{ README.rdoc CHANGELOG LICENSE }
69
+
70
+ s.rubyforge_project = 'madderlib'
71
+ s.rubygems_version = '1.1.1'
72
+ end
@@ -0,0 +1,69 @@
1
+ require File.join(File.dirname(__FILE__), 'spec_helper')
2
+
3
+ require 'benchmark'
4
+ include Benchmark
5
+
6
+
7
+
8
+ =begin
9
+ class Bencher
10
+ attr_accessor :debug
11
+
12
+ def call(name='(none)', &block)
13
+ x = nil
14
+ unless @debug
15
+ x = block.call
16
+ else
17
+ bm(6) do |reporter|
18
+ reporter.report(name) { x = block.call }
19
+ end
20
+ end
21
+ x
22
+ end
23
+ end
24
+
25
+ BENCHER = Bencher.new
26
+ =end
27
+
28
+
29
+
30
+ describe MadderLib, "benchmarking" do
31
+
32
+ it "conditional logic" do
33
+ builder = madderlib :split_3_2_1 do
34
+ say('flopsy').or(2).say('mopsy').or(3).say('cottontail')
35
+ say('ate')
36
+ say('many').times(2, 4)
37
+ %w{ cherries churches rocks ducks }.each do |wrong|
38
+ say(wrong).if { false }
39
+ end
40
+ say('carrots')
41
+ end
42
+
43
+ =begin
44
+ bm(6) do |reporter|
45
+ reporter.report(time.to_s) do
46
+ end
47
+ end
48
+ =end
49
+
50
+ delta = nil
51
+ (1..10).each do |time|
52
+ # fake progress
53
+ print '^'
54
+
55
+ t = Time.now.to_f
56
+
57
+ pound_on do
58
+ words = builder.words
59
+ end
60
+
61
+ # the operations should take a very similar amount of time
62
+ # no degradation, like we saw with Generator
63
+ # still, we need to be generous (1/2 sec)
64
+ t = (Time.now.to_f - t)
65
+ t.should be_close(delta, 0.5) if delta
66
+ delta = t
67
+ end
68
+
69
+ end