poefy 0.5.1

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.
@@ -0,0 +1,268 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # Handle the procedural generation of poems.
6
+ ################################################################################
7
+
8
+ require 'timeout'
9
+
10
+ ################################################################################
11
+
12
+ module Poefy
13
+
14
+ module Generation
15
+
16
+ # Generate specific poem types.
17
+ def poem poetic_form = @poetic_form
18
+
19
+ if !@db.exists?
20
+ return handle_error 'ERROR: Database does not yet exist', nil
21
+ end
22
+
23
+ # Validate the poetic form hash.
24
+ raise ArgumentError, 'Argument must be a hash' unless
25
+ poetic_form.is_a?(Hash)
26
+ poetic_form = validate_poetic_form poetic_form
27
+
28
+ # Make sure the hash contains ':form' or ':rhyme' keys.
29
+ if !(poetic_form[:form] or poetic_form[:rhyme])
30
+ return handle_error \
31
+ "ERROR: No valid rhyme or form option specified.\n" +
32
+ " Try again using the -f or -r option.\n", nil
33
+ end
34
+
35
+ # Loop until we find a valid poem.
36
+ output = poem_proper_sentence poetic_form
37
+
38
+ # Return nil if poem could not be created.
39
+ return nil if (output.nil? or output == [] or output == [''])
40
+
41
+ # Indent the output using the :indent string.
42
+ output = do_indent(output, get_poetic_form_indent(poetic_form))
43
+
44
+ # Append nil lines to the end if the :rhyme demands it.
45
+ rhyme = tokenise_rhyme get_poetic_form_rhyme poetic_form
46
+ (output.length...rhyme.length).each do |i|
47
+ output[i] = ''
48
+ end
49
+
50
+ output
51
+ end
52
+
53
+ # Get all rhyming lines for the word.
54
+ def rhymes word, key = nil
55
+ return nil if word.nil?
56
+ sproc = @db.db.prepare %Q[
57
+ SELECT rhyme, final_word, syllables, line
58
+ FROM lines
59
+ WHERE rhyme = ?
60
+ ORDER BY rhyme, final_word, syllables, line
61
+ ]
62
+ output = word.to_phrase.rhymes.keys.map do |rhyme|
63
+ sproc.reset!
64
+ sproc.bind_param(1, rhyme)
65
+ sproc.execute.to_a
66
+ end.flatten
67
+ sproc.close
68
+ if !key.nil? and %w[rhyme final_word syllables line].include?(key)
69
+ output.map!{ |i| i[key] }
70
+ end
71
+ output
72
+ end
73
+
74
+
75
+ private
76
+
77
+ # Generate a poem using the database and a poem format.
78
+ # If ':proper' then loop to find a valid poem.
79
+ def poem_proper_sentence poetic_form = @poetic_form
80
+ poetic_form = validate_poetic_form poetic_form
81
+ output, count_down = [], 500
82
+
83
+ # Final line must close with sentence-end punctuation.
84
+ # Don't start poems with these words.
85
+ banned_first_words = poetic_form[:proper] ? %w{and but or nor yet} : []
86
+
87
+ loop do
88
+ output = gen_poem_using_conditions poetic_form
89
+ break if output.nil? or output.empty?
90
+ if !banned_first_words.include?(first_word(output[0]).downcase) and
91
+ ( !poetic_form[:proper] or has_stop_punctuation?(output[-1]) )
92
+ break
93
+ end
94
+
95
+ # Fail after some number of failures.
96
+ if (count_down -= 1) == 0 and poetic_form[:proper]
97
+ return handle_error \
98
+ "ERROR: Proper sentence structure not able to be honoured.\n" +
99
+ " Ensure the input has closing punctuation or\n" +
100
+ " try again using the -p option."
101
+ end
102
+ end
103
+
104
+ # (previous way of dealing with closing full stops)
105
+ # (leaving the code commented out for now)
106
+ # output[-1] = end_the_sentence(output[-1])
107
+ output
108
+ end
109
+
110
+ # Use the constraints in 'poetic_form' to generate a poem.
111
+ def gen_poem_using_conditions poetic_form = @poetic_form
112
+ poetic_form = poetic_form_full poetic_form
113
+ poetic_form = validate_poetic_form poetic_form
114
+
115
+ # Tokenise the rhyme string, and return [] if invalid.
116
+ tokenised_rhyme = tokenise_rhyme poetic_form[:rhyme]
117
+ if tokenised_rhyme == []
118
+ return handle_error 'ERROR: Rhyme string is not valid', []
119
+ end
120
+
121
+ # Add lines number as ':line' in each element's hash.
122
+ by_line = conditions_by_line(tokenised_rhyme, poetic_form)
123
+
124
+ # Now we have ':line', so we can break the array order.
125
+ # Let's get rid of empty lines, and group by the rhyme letter.
126
+ conditions_by_rhyme = by_line.reject do |i|
127
+ i[:rhyme] == ' '
128
+ end.group_by do |i|
129
+ i[:rhyme_letter]
130
+ end
131
+
132
+ # Okay, this is great. But if we're making villanelles we'll need
133
+ # duplicated refrain lines. So we won't need unique rhymes for those.
134
+ # So make a distinct set of lines conditions, still grouped by rhyme.
135
+ # This will be the same as [conditions_by_rhyme], except duplicate lines
136
+ # are removed. (These are lines with capitals and numbers: i.e. A1, B2)
137
+ # It will keep the condition hash of only the first refrain line.
138
+ distinct_line_conds = Hash.new { |h,k| h[k] = [] }
139
+ conditions_by_rhyme.each do |key, values|
140
+ uppers = []
141
+ values.each do |v|
142
+ char_1 = v[:rhyme][0]
143
+ if char_1 == char_1.upcase
144
+ if !uppers.include?(v[:rhyme])
145
+ uppers << v[:rhyme]
146
+ distinct_line_conds[key] << v
147
+ end
148
+ else
149
+ distinct_line_conds[key] << v
150
+ end
151
+ end
152
+ end
153
+
154
+ # Right, let's now loop through each rhyme group and find all from
155
+ # the database where the number of lines can be fulfilled.
156
+
157
+ # First, get the order of rhymes, from most to least.
158
+ distinct_line_conds = distinct_line_conds.sort_by{ |k,v| v.count }.reverse
159
+
160
+ # This will store the rhymes that have already been used in the poem.
161
+ # This is so we do not duplicate rhymes between distinct rhyme letters.
162
+ rhymes_already_used = []
163
+
164
+ # This is the final set of lines.
165
+ all_lines = []
166
+
167
+ # Loop through each rhyme group to find lines that satisfy the conditions.
168
+ distinct_line_conds.each do |rhyme_letter, line_conds|
169
+
170
+ # The conditions that will be passed to '#conditional_selection'.
171
+ # This is an array of procs, one for each line.
172
+ conditions = line_conds.map do |cond|
173
+ proc { |arr, elem| diff_end(arr, elem) and validate_line(elem, cond)}
174
+ end
175
+
176
+ # Get all rhymes from the database with at least as many final
177
+ # words as there are lines to be matched.
178
+ rhymes = nil
179
+
180
+ # If all the lines include a 'syllable' condition,
181
+ # then we can specify to only query for matching lines.
182
+ if line_conds.all?{ |i| i[:syllable] }
183
+ min = line_conds.min do |a, b|
184
+ [*a[:syllable]].min <=> [*b[:syllable]].min
185
+ end[:syllable]
186
+ max = line_conds.max do |a, b|
187
+ [*a[:syllable]].max <=> [*b[:syllable]].max
188
+ end[:syllable]
189
+ min_max = { min: [*min].min, max: [*max].max }
190
+ min_max = nil if min_max[:max] == 0
191
+ rhymes = @db.sproc_rhymes_all!(line_conds.count, min_max)
192
+ else
193
+ rhymes = @db.sproc_rhymes_all!(line_conds.count)
194
+ end
195
+
196
+ # Get just the rhyme part of the hash.
197
+ rhymes = rhymes.map{ |i| i['rhyme'] }
198
+ rhymes = rhymes - rhymes_already_used
199
+
200
+ # For each rhyme, get all lines and try to sastify all conditions.
201
+ out = []
202
+ rhymes.shuffle.each do |rhyme|
203
+ out = try_rhyme(conditions, rhyme)
204
+ break if !out.empty?
205
+ end
206
+ if out.empty?
207
+ return handle_error 'ERROR: Not enough rhyming lines in the input'
208
+ end
209
+ rhymes_already_used << out.first['rhyme']
210
+
211
+ # Add the line number back to the array.
212
+ line_conds.count.times do |i|
213
+ out[i]['line_number'] = line_conds[i][:line]
214
+ end
215
+
216
+ out.each do |i|
217
+ all_lines << i
218
+ end
219
+ end
220
+
221
+ # Transpose lines to their actual location.
222
+ poem_lines = []
223
+ all_lines.each do |line|
224
+ poem_lines[line['line_number'] - 1] = line['line']
225
+ end
226
+
227
+ # Go back to the [by_line] array and find all the refrain line nos.
228
+ refrains = Hash.new { |h,k| h[k] = [] }
229
+ by_line.reject{ |i| i[:rhyme] == ' ' }.each do |line|
230
+ if line[:rhyme][0] == line[:rhyme][0].upcase
231
+ refrains[line[:rhyme]] << line[:line]
232
+ end
233
+ end
234
+ refrains.keys.each do |k|
235
+ refrains[k].sort!
236
+ end
237
+
238
+ # Use the first refrain line and repeat it for the others.
239
+ refrains.each do |key, values|
240
+ values[1..-1].each do |i|
241
+ poem_lines[i-1] = poem_lines[values.first-1]
242
+ end
243
+ end
244
+
245
+ poem_lines
246
+ end
247
+
248
+ # Loop through the rhymes until we find one that works.
249
+ # (In a reasonable time-frame)
250
+ def try_rhyme conditions, rhyme
251
+ output = []
252
+ lines = @db.sproc_lines_all!(rhyme)
253
+ begin
254
+ Timeout::timeout(2) do
255
+ output = conditional_selection(lines.shuffle, conditions)
256
+ break
257
+ end
258
+ rescue
259
+ output = []
260
+ end
261
+ output
262
+ end
263
+
264
+ end
265
+
266
+ end
267
+
268
+ ################################################################################
@@ -0,0 +1,27 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # Handle error message.
6
+ # Quit the program if called from console.
7
+ ################################################################################
8
+
9
+ module Poefy
10
+
11
+ module HandleError
12
+
13
+ private
14
+
15
+ def handle_error msg, return_value = nil
16
+ if @console
17
+ STDERR.puts msg
18
+ exit 1
19
+ end
20
+ return_value
21
+ end
22
+
23
+ end
24
+
25
+ end
26
+
27
+ ################################################################################
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env ruby
2
+ # Encoding: UTF-8
3
+
4
+ ################################################################################
5
+ # Base internals for the PoefyGen class.
6
+ ################################################################################
7
+
8
+ require 'json'
9
+
10
+ ################################################################################
11
+
12
+ module Poefy
13
+
14
+ module PoefyGenBase
15
+
16
+ attr_reader :console, :db, :local, :overwrite
17
+
18
+ def initialize db_name, options = {}
19
+ handle_options options
20
+ @db = Poefy::Database.new get_database_file(db_name), @console
21
+ end
22
+
23
+ # Make a database using the given lines.
24
+ def make_database input, overwrite = @overwrite
25
+ if overwrite
26
+ make_database! input
27
+ else
28
+ @db.make_new validate_lines input
29
+ end
30
+ end
31
+ def make_database! input
32
+ @db.make_new! validate_lines input
33
+ end
34
+
35
+ # Close the database.
36
+ def close
37
+ @db.close
38
+ end
39
+
40
+ # Validate the lines. Arg could be a filename,
41
+ # newline delimited string, or array of lines.
42
+ def validate_lines input
43
+
44
+ # If the input is a file, then read it.
45
+ lines = File.exists?(input) ? File.read(input) : input
46
+
47
+ # If lines is not an array, assume string and split on newlines.
48
+ lines = lines.respond_to?(:each) ? lines : lines.split("\n")
49
+ lines.map(&:strip!)
50
+ lines
51
+ end
52
+
53
+ private
54
+
55
+ # Find the correct database file.
56
+ # If local, just use the value.
57
+ # Else, use the database in /data/ directory.
58
+ def get_database_file database_name
59
+ if @local
60
+ database_name
61
+ else
62
+ path = File.expand_path('../../../data', __FILE__)
63
+ file = File.basename(database_name, '.db')
64
+ path + '/' + file + '.db'
65
+ end
66
+ end
67
+
68
+ # Handle the optional initialize options hash.
69
+ def handle_options options
70
+ @console = options[:console] || false
71
+ @overwrite = options[:overwrite] || false
72
+ @local = options[:local] || false
73
+ @poetic_form = {}
74
+ @poetic_form[:proper] = options[:proper] || true
75
+ @poetic_form = validate_poetic_form options
76
+ end
77
+
78
+ # Make sure the options hash is in order.
79
+ def validate_poetic_form poetic_form
80
+ input, output = poetic_form, {}
81
+ form_string = get_valid_form input[:form]
82
+
83
+ # Handle obvious inputs.
84
+ output[:form] = form_string if form_string
85
+ output[:rhyme] = input[:rhyme] if input[:rhyme]
86
+ output[:indent] = input[:indent] if input[:indent]
87
+ output[:syllable] = input[:syllable] if input[:syllable]
88
+ output[:regex] = input[:regex] if input[:regex]
89
+
90
+ # Tokenise string to arrays and hashes.
91
+ rhyme = get_poetic_form_rhyme(output)
92
+ if output[:rhyme]
93
+ output[:rhyme] = tokenise_rhyme output[:rhyme]
94
+ end
95
+ if output[:syllable]
96
+ output[:syllable] = transform_string_syllable output[:syllable], rhyme
97
+ end
98
+ if output[:regex]
99
+ output[:regex] = transform_string_regex output[:regex], rhyme
100
+ end
101
+
102
+ # Get from instance by default.
103
+ output[:proper] = input[:proper].nil? ?
104
+ @poetic_form[:proper] : input[:proper]
105
+
106
+ # Tiny amendment to solve later errors.
107
+ output[:rhyme] = ' ' if output[:rhyme] == ''
108
+ output
109
+ end
110
+
111
+ # Handle error message. Quit the program if called from console.
112
+ def handle_error msg
113
+ if @console
114
+ STDERR.puts msg
115
+ exit 1
116
+ end
117
+ nil
118
+ end
119
+
120
+ end
121
+
122
+ end
123
+
124
+ ################################################################################