module Sham
  @@values     = {}
  @@offsets    = {}
  @@generators = {}
  
  # Over-ride module's built-in name method, so we can re-use it for
  # generating names. This is a bit of a no-no, but we get away with
  # it in this context.
  def self.name(*args, &block)
    method_missing(:name, *args, &block)
  end
  
  def self.method_missing(symbol, *args, &block)
    if block_given?
      @@generators[symbol] = block
      @@values[symbol] = generate_values(12, &block)
    else
      fetch_value(symbol)
    end
  end

  def self.reset
    @@offsets = {}
  end
  
private

  def self.fetch_value(symbol)
    raise "No sham defined for #{symbol}" if @@values[symbol].nil?
    offset = @@offsets[symbol] || 0
    @@offsets[symbol] = offset + 1
    if offset >= @@values[symbol].length
      @@values[symbol] = generate_values(2 * @@values[symbol].length, &@@generators[symbol])
      raise "Can't generate more unique values for Sham.#{symbol}" if offset >= @@values[symbol].length
    end
    @@values[symbol][offset]
  end
  
  def self.generate_values(count, &block)
    seeded { (1..count).map(&block).uniq }
  end
  
  def self.seeded
    old_seed = srand(1)
    result = yield
    srand(old_seed)
    result
  end
end

module Sham
  @@values     = {}
  @@offsets    = {}
  @@generators = {}
  
  # Over-ride module's built-in name method, so we can re-use it for
  # generating names. This is a bit of a no-no, but we get away with
  # it in this context.
  def self.name(*args, &block)
    method_missing(:name, *args, &block)
  end
  
  def self.method_missing(symbol, *args, &block)
    if block_given?
      @@generators[symbol] = block
      @@values[symbol] = generate_values(12, &block)
    else
      fetch_value(symbol)
    end
  end

  def self.reset
    @@offsets = {}
  end
  
private

  def self.fetch_value(symbol)
    raise "No sham defined for #{symbol}" if @@values[symbol].nil?
    offset = @@offsets[symbol] || 0
    @@offsets[symbol] = offset + 1
    if offset >= @@values[symbol].length
      @@values[symbol] = generate_values(2 * @@values[symbol].length, &@@generators[symbol])
      raise "Can't generate more unique values for Sham.#{symbol}" if offset >= @@values[symbol].length
    end
    @@values[symbol][offset]
  end
  
  def self.generate_values(count, &block)
    seeded { (1..count).map(&block).uniq }
  end
  
  def self.seeded
    srand(1)
    result = yield
    srand
    result
  end
end

Make sure we don't reset the sequence returned by rand outside Sham.
Make sure we don't reset the sequence returned by rand outside Sham.

Turns out Ruby's srand doesn't work the way you'd think. The seed value it returns is the initial value used to see the generator, not the current seed value. That means you can't use it to save and restore your spot in the sequence.
