madderlib 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|