class Andor::NameGenerator

A Demiurge NameGenerator takes a set of rules for names and creates one or more randomly from that ruleset.

@since 0.4.0

Constants

NAME_REGEXP

Regular expression for legal names

@since 0.4.0

Attributes

randomizer[RW]
rules[R]

Public Class Methods

new() click to toggle source

Create a new generator with an empty ruleset

@since 0.4.0

# File lib/andor.rb, line 75
def initialize
  @rules = {}
  @randomizer = Random.new(Time.now.to_i)
end

Public Instance Methods

evaluate_ast(ast, name: "some name") click to toggle source

private

# File lib/andor.rb, line 133
def evaluate_ast(ast, name: "some name")
  # Let's grow out a Parslet-based evaluator to remove the outdated evaluation code below.
  if ast.is_a?(Hash)
    if ast.has_key?(:str_const)
      return ast[:str_const]
    elsif ast.has_key?(:str_val)
      return ast[:str_val].map { |h| h[:char] }.join
    elsif ast.has_key?(:name)
      return generate_from_name(ast[:name].to_s)
    else
      raise ::Andor::Errors::BadlyFormedGeneratorRule.new("Malformed rule internal structure: (Hash) #{ast.inspect}!")
    end
  elsif ast.is_a?(Array)
    if ast[0].has_key?(:left)
      if ast[1].has_key?(:plus)
        left_side = evaluate_ast(ast[0][:left])
        return ast[1..-1].map { |term| evaluate_ast(term[:right]) }.inject(left_side, &:+)
      elsif ast[1].has_key?(:bar)
        left_prob = ast[0][:left_prob] ? ast[0][:left_prob][:prob].to_f : 1.0
        choice_prob = [left_prob] + ast[1..-1].map { |term| term[:right_prob] ? term[:right_prob][:prob].to_f : 1.0 }

        unless choice_prob.all? { |p| p.is_a?(Float) }
          raise ::Andor::Errors::BadlyFormedGeneratorRule.new("Probability isn't a float: #{choice_prob.select { |p| !p.is_a?(Float) }.inspect}!")
        end
        total_prob = choice_prob.inject(0.0, &:+)
        if total_prob < 0.000001
          raise ::Andor::Errors::BadlyFormedGeneratorRule.new("Total probability less than epsilon: #{total_prob.inspect}!")
        end
        r = @randomizer.rand(total_prob)

        # Subtract probability from our random sample until we get that far into the CDF
        cur_index = 0
        while cur_index < choice_prob.size && r >= choice_prob[cur_index]
          r -= choice_prob[cur_index]
          cur_index += 1
        end
        # Shouldn't hit this, but just in case...
        cur_index = (choice_prob.size - 1) if cur_index >= choice_prob.size
        if cur_index == 0
          bar_choice = evaluate_ast(ast[0][:left]).to_s
        else
          bar_choice = evaluate_ast(ast[cur_index][:right]).to_s
        end

        return bar_choice
      else
        raise ::Andor::Errors::BadlyFormedGeneratorRule.new("Malformed rule internal structure: (Array/op) #{ast.inspect}!")
      end
    else
      raise ::Andor::Errors::BadlyFormedGeneratorRule.new("Malformed rule internal structure: (Array) #{ast.inspect}!")
    end
  else
    raise ::Andor::Errors::BadlyFormedGeneratorRule.new("Malformed rule internal structure: (#{ast.class}) #{ast.inspect}!")
  end

  if ast[0] == "|"
    choices = ast[1..-1]
    probabilities = choices.map { |choice| choice[0] == :prob ? choice[1] : 1.0 }
    total = probabilities.inject(0.0, &:+)
    chosen = rand() * total

    index = 0
    while chosen > probabilities[index] && index < choices.size
      chosen -= probabilities[index]
      index += 1
    end
    #STDERR.puts "Chose #{index} / #{choices[index].inspect} from #{choices.inspect}"
    return evaluate_ast choices[index]
  elsif ast[0] == "+"
    return ast[1..-1].map { |elt| evaluate_ast(elt) }.join("")
  elsif ast[0] == :prob
    raise ::Andor::Errors::BadlyFormedGeneratorRule.new("Not supposed to directly evaluate probability rule!")
  else
    raise ::Andor::Errors::BadlyFormedGeneratorRule.new("Malformed rule internal structure: #{ast.inspect}!")
  end
end
generate_from_name(name) click to toggle source
# File lib/andor.rb, line 122
def generate_from_name(name)
  unless @rules.has_key?(name)
    STDERR.puts "Known rules: #{@rules.keys.inspect}"
    raise ::Andor::Errors::NoSuchNameInGenerator.new("Unknown name #{name.inspect} in generator!")
  end

  evaluate_ast @rules[name], name: name
end
load_rules_from_andor_string(rules) click to toggle source

Add rules to this generator from the given string

@attr [String] rules The block of rule content in DemiRule format. @return [void] @since 0.4.0

# File lib/andor.rb, line 93
def load_rules_from_andor_string(rules)
  defn_parser = AndorDefnParser.new

  rules.split("\n").each_with_index do |line, line_no|
    content, _ = line.split("#", 2)
    next if content == nil
    next if content.strip == ""
    name, defn = content.split(":", 2)
    unless name && defn
      raise ::Andor::Errors::DemiRuleFormatError.new("Badly-formed name definition line in DemiRule format on line #{line_no.inspect}")
    end
    unless name =~ NAME_REGEXP
      raise ::Andor::Errors::DemiRuleFormatError.new("Illegal name #{name.inspect} in DemiRule format on line #{line_no.inspect}")
    end
    if @rules[name]
      raise ::Andor::Errors::DemiRuleFormatError.new("Duplicate name #{name.inspect} in DemiRule format on line #{line_no.inspect}")
    end

    begin
      symbols = defn_parser.parse(defn)
    rescue Parslet::ParseFailed => error
      raise ::Andor::Errors::DemiRuleFormatError.new("Can't parse Andor name definition for #{name.inspect}", cause: error)
    end

    @rules[name] = symbols  # Need to transform to proper ast
  end
  nil
end
names() click to toggle source

Return all names currently defined by rules.

@return [Array<String>] Array of all names. @since 0.4.0

# File lib/andor.rb, line 84
def names
  @rules.keys
end