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