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,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