module Poefy::PoeticForms

Constants

POETIC_FORMS

If the token is an array, then a random sample will be used.

Public Instance Methods

acrostic(word) click to toggle source

Create a regex specification for acrostics.

acrostic('unintelligible')
acrostic('unin tell igib le')
# File lib/poefy/poetic_forms.rb, line 125
def acrostic word
  output = {}
  word.split('').each.with_index do |char, i|
    output[i + 1] = /^[#{char.downcase}]/i if char != ' '
  end
  output
end
acrostic_x(word) click to toggle source

Create a regex specification for acrostics. Uses special logic for 'X'. Match words starting 'ex' and then change case to 'eX'.

# File lib/poefy/poetic_forms.rb, line 136
def acrostic_x word
  regex = {}
  transform = {}
  word.split('').each.with_index do |char, i|
    if char.downcase == 'x'
      regex[i + 1] = /^ex/i
      transform[i + 1] = proc do |line|
        line[0..1] = 'eX'
        ' ' + line
      end
    elsif char != ' '
      regex[i + 1] = /^[#{char.downcase}]/i
      transform[i + 1] = proc do |line|
        '  ' + line
      end
    end
  end
  { regex: regex, transform: transform }
end

Private Instance Methods

do_indent(lines, str) click to toggle source

Indent an array of lines using a string of numbers.

# File lib/poefy/poetic_forms.rb, line 256
def do_indent lines, str
  return lines if str.nil? or lines.nil? or lines.empty?

  # Convert the indent string into an array.
  indent_arr = (str + '0' * lines.length).split('')
  indent_arr = indent_arr.each_slice(lines.length).to_a[0]

  # Convert to integers. Spaces should be zero.
  indent_arr.map! { |i| Integer(i) rescue 0 }

  # Zip, iterate, and prepend indent.
  indent_arr.zip(lines).map do |line|
    '  ' * line[0] + (line[1] ? line[1] : '')
  end
end
expand_hash_keys(type, input, tokens, default) click to toggle source

Convert non-positive-integer keys into the correct position.

# File lib/poefy/poetic_forms.rb, line 458
def expand_hash_keys type, input, tokens, default
  output = input.dup
  line_count = tokens.length

  # Handle negative keys.
  output.keys.each do |k|
    if k.is_a?(Numeric) and k < 0
      line = line_count + 1 + k
      output[line] = output[k]
    end
  end

  # Find all lines that are not empty.
  content_lines = tokens.map.with_index do |v, i|
    i + 1 if (v[:token].strip != '')
  end.compact

  # Handle modulo lines.
  # Handle 'e' even and 'o' odd lines.
  modulo_lines = {}
  output.keys.each do |k|
    is_modulo = k.respond_to?(:include?) && k.include?('m')
    is_even_odd = %w[e o].include?(k)
    if is_modulo or is_even_odd
      if is_modulo
        vals = k.split('m').map(&:to_i)
        divider = vals.first.to_i.abs
        remainder = vals.last.to_i.abs
        if divider == 0
          # Raise a SyllableError or RegexError.
          msg = "#{type.capitalize} hash invalid,"
          msg += " key='#{k}', modulo='#{divider}m#{remainder}'"
          e = Object.const_get("Poefy::#{type.capitalize}Error")
          raise e.new(msg)
        end
      elsif is_even_odd
        divider = 2
        remainder = (k == 'e') ? 0 : 1
      end
      content_lines.modulo_index(divider, remainder, 1).each do |i|
        modulo_lines[i] = output[k]
      end
    end
  end

  # Take {modulo_lines} as the base and overwrite it with specified keys.
  if modulo_lines
    output.keys.each do |k|
      modulo_lines[k] = output[k]
    end
    output = modulo_lines
  end

  # Go through each line and make sure there is a value for each.
  # Use default if there is no specific value.
  default_value = output[0] ? output[0] : default
  (1..line_count).each do |i|
    output[i] = default_value if output[i].nil?
  end

  # Remove keys that are not numeric, or are less than or equal to zero.
  output.reject!{ |k| !k.is_a?(Numeric) or k <= 0 }

  # Return sorted hash.
  sort_hash output
end
get_poetic_form_indent(poetic_form = @poetic_form) click to toggle source
# File lib/poefy/poetic_forms.rb, line 193
def get_poetic_form_indent poetic_form = @poetic_form
  get_poetic_form_token :indent, poetic_form
end
get_poetic_form_rhyme(poetic_form = @poetic_form) click to toggle source
# File lib/poefy/poetic_forms.rb, line 190
def get_poetic_form_rhyme poetic_form = @poetic_form
  get_poetic_form_token :rhyme, poetic_form
end
get_poetic_form_rhyme_longest(poetic_form = @poetic_form) click to toggle source

If the token is specified in the hash, return it,

else get the token for the named form.
# File lib/poefy/poetic_forms.rb, line 187
def get_poetic_form_rhyme_longest poetic_form = @poetic_form
  get_poetic_form_token :rhyme, poetic_form, true
end
get_poetic_form_token(token, poetic_form = @poetic_form, longest = false) click to toggle source
# File lib/poefy/poetic_forms.rb, line 196
def get_poetic_form_token token,
                          poetic_form = @poetic_form,
                          longest = false
  if poetic_form.empty?
    ' '
  elsif poetic_form[token]
    poetic_form[token]
  elsif poetic_form[:form].nil?
    ' '
  elsif POETIC_FORMS[poetic_form[:form].to_sym].nil?
    ' '
  else
    token = POETIC_FORMS[poetic_form[:form].to_sym][token]
    if token.is_a?(Array)
      token = longest ? token.max_by(&:length) : token.sample
    end
    token
  end
end
get_valid_form(form_name) click to toggle source

Make sure the form name is in the list.

# File lib/poefy/poetic_forms.rb, line 164
def get_valid_form form_name
  return nil if form_name.nil?
  POETIC_FORMS[form_name.to_sym] ? form_name.to_sym : nil
end
is_int?(str) click to toggle source

Can the string be converted to integer?

# File lib/poefy/poetic_forms.rb, line 159
def is_int? str
  !(Integer(str) rescue nil).nil?
end
poetic_form_full(poetic_form = @poetic_form) click to toggle source

Get full form, from either the user-specified options,

or the default poetic form.
# File lib/poefy/poetic_forms.rb, line 171
def poetic_form_full poetic_form = @poetic_form
  rhyme     = get_poetic_form_token :rhyme,     poetic_form
  indent    = get_poetic_form_token :indent,    poetic_form
  syllable  = get_poetic_form_token :syllable,  poetic_form
  regex     = get_poetic_form_token :regex,     poetic_form
  transform = get_poetic_form_token :transform, poetic_form
  poetic_form[:rhyme]     = rhyme
  poetic_form[:indent]    = indent    if indent    != ''
  poetic_form[:syllable]  = syllable  if syllable  != ''
  poetic_form[:regex]     = regex     if regex
  poetic_form[:transform] = transform if transform != ' '
  poetic_form
end
range_to_array(input) click to toggle source

Convert a range in the string form “1-6” to an array. Assumes elements are integers.

# File lib/poefy/poetic_forms.rb, line 283
def range_to_array input
  return input if input.is_a?(Numeric) || !input.include?('-')
  vals = input.split('-').map(&:to_i).sort
  (vals.first..vals.last).to_a
end
sort_hash(input) click to toggle source

Sort by keys, to make it more human-readable.

# File lib/poefy/poetic_forms.rb, line 273
def sort_hash input
  output = {}
  input.keys.sort.each do |k|
    output[k] = input[k]
  end
  output
end
string_to_array(input) click to toggle source

Convert an array in the string form “4,6,8-10,12” to an array. Assumes elements are positive integers.

# File lib/poefy/poetic_forms.rb, line 291
def string_to_array input
  return [input.to_i.abs] if input.is_a?(Numeric)
  arr = input.is_a?(Array) ? input : input.split(',')

  # Convert to positive integers, and remove duplicates.
  output = arr.map do |i|
    range_to_array(i)
  end.flatten.map do |i|
    i.to_i.abs
  end.sort.uniq.select do |i|
    i != 0
  end

  # This cannot be an empty array []. It will fail anyway when we
  #   come to do the poem generation, but it's better to fail now.
  raise Poefy::SyllableError.new if output.empty?
  output
end
tokenise_rhyme(rhyme_string) click to toggle source

Turn a rhyme format string into a usable array of tokens. Example formats:

sonnet_form     = 'abab cdcd efef gg'
villanelle_form = 'A1bA2 abA1 abA2 abA1 abA2 abA1A2'
# File lib/poefy/poetic_forms.rb, line 220
def tokenise_rhyme rhyme_string
  return rhyme_string if rhyme_string.is_a? Array

  tokens = []
  buffer = ''
  rhyme_string.split('').each do |char|
    if !numeric?(char) and buffer != ''
      tokens << buffer
      buffer = ''
    end
    buffer += char
  end
  tokens << buffer

  # Handle invalid tokens.
  # ["a1"] ["1"] ["1122"] [" 1"] [" 11"] [":1"]
  boolean_array = tokens.map do |i|
    keep = i.gsub(/[^A-Z,0-9]/,'')
    (keep == '' or !is_int?(keep))
  end
  valid = boolean_array.reduce{ |sum, i| sum && i }
  raise Poefy::RhymeError unless valid
  tokens = [' '] if tokens == ['']

  # Output as a hash.
  tokens.map do |i|
    hash = {
      token: i,
      rhyme_letter: i[0].downcase
    }
    hash[:refrain] = i if i[0] == i[0].upcase
    hash
  end
end
transform_input_regex(input, rhyme) click to toggle source

Do the same for regular expression strings.

# File lib/poefy/poetic_forms.rb, line 328
def transform_input_regex input, rhyme
  tokens = tokenise_rhyme rhyme
  hash = transform_input_to_hash :regex, input
  hash = validate_hash_values :regex, hash
  hash = expand_hash_keys :regex, hash, tokens, //
end
transform_input_syllable(input, rhyme) click to toggle source

'10' '9,10,11' '[8,8,5,5,8]' '[[8,9],,[4,5,6],,[8,9]]' '{1:8,2:8,3:5,4:5,5:8}' '{1:,2:,3:,4:,5:}' '{0:,3:,4:}' '{1:8,5:8}' '{1:8,2:8,3:5,-2:5,-1:8}' Use the rhyme string as base for the number of lines in total.

# File lib/poefy/poetic_forms.rb, line 320
def transform_input_syllable input, rhyme
  tokens = tokenise_rhyme rhyme
  hash = transform_input_to_hash :syllable, input
  hash = validate_hash_values :syllable, hash
  hash = expand_hash_keys :syllable, hash, tokens, 0
end
transform_input_to_hash(type, input) click to toggle source

This should work for both syllable and regex strings. It should also be fine for Integer and Regexp 'input' values.

# File lib/poefy/poetic_forms.rb, line 337
def transform_input_to_hash type, input
  return input if input.is_a? Hash

  # Don't go any further if we've got an invalid type.
  valid_non_string =
    input.is_a?(Array) ||
    (type == :syllable and input.is_a?(Numeric)) ||
    (type == :regex and input.is_a?(Regexp))
  valid_string_like = !valid_non_string && input.respond_to?(:to_s)
  raise TypeError unless valid_non_string || valid_string_like

  # Perform different tasks depending on type.
  input.strip! if input.is_a? String
  input = input.to_i if input.is_a? Numeric
  input = input.to_s if valid_string_like
  return {} if input == ''

  # This will be built up over the course of the method.
  output = {}

  # Figure out datatype.
  # Regex string input cannot be an array, but syllable can.
  datatype = :string
  if !input.is_a?(Regexp)
    if input.is_a?(Array)
      datatype = :array
    elsif type == :syllable and input[0] == '[' and input[-1] == ']'
      datatype = :array
    elsif input[0] == '{' and input[-1] == '}'
      datatype = :hash
    end
  end

  # If it's a basic string format, convert it to hash.
  if datatype == :string

    # Regex cannot be an array or range, but syllable can.
    if type == :regex
      arr = (input == []) ? [] : [Regexp.new(input)]

    # Special case for if a user explicitly states only '0'.
    elsif type == :syllable
      arr = input == '0' ? [0] : string_to_array(input)
    end

    # Set this to be the default '0' hash value.
    arr = arr.first if arr.count == 1
    output = { 0 => arr }
    datatype = :hash

  # If it's wrapped in [] or {}, then evaluate it using YAML.
  else

    # Don't need to evaluate if it's already an Array.
    if input.is_a?(Array)
      output = input
    else
      begin
        # If it's a regex, mandate the ': ' key separator.
        # (This is so the string substitutions don't mess up the regex.)
        # If it's a syllable, we can be more flexible with gsubs.
        as_yaml = input
        if type == :syllable
          as_yaml = input.gsub(':', ': ').gsub('=>', ': ')
        end
        output = YAML.load(as_yaml)
      rescue
        # Raise a SyllableError or RegexError.
        msg = "#{type.capitalize} hash is not valid YAML"
        e = Object.const_get("Poefy::#{type.capitalize}Error")
        raise e.new(msg)
      end
    end
  end

  # Convert array to positioned hash.
  if datatype == :array
    output = output.map.with_index do |e, i|
      [i+1, e]
    end.to_h
  end

  output
end
validate_hash_values(type, input) click to toggle source

Run different methods on each value depending on the type. If it's a syllable, convert all values to int arrays. If it's a regex, convert all values to regexp.

# File lib/poefy/poetic_forms.rb, line 425
def validate_hash_values type, input
  format_value = if type == :syllable
    Proc.new do |x|
      arr = string_to_array(x)
      arr.count == 1 ? arr.first : arr
    end
  elsif type == :regex
    Proc.new do |x|
      x.is_a?(Regexp) ? x : Regexp.new(x.to_s)
    end
  end

  # Validate values.
  if input.is_a?(Hash)
    input.each do |k, v|
      begin
        input[k] = format_value.call(v)
      rescue
        # Raise a SyllableError or RegexError.
        msg = "#{type.capitalize} hash invalid, key='#{k}' value='#{v}'"
        e = Object.const_get("Poefy::#{type.capitalize}Error")
        raise e.new(msg)
      end
    end
  elsif input.is_a?(Array)
    input.map! do |i|
      i = format_value.call(i)
    end
  end
  input
end