#!/usr/bin/env ruby
# A modification of Joel VanderWerf's EnumerableOperator#product to include 
# restrictions ('select' style filters that are applied as the product is
# constructed).
# 
# The argument list consists of a mixture of factors (Enumerable objects)
# and restrictions (procs returning a boolean). 
#
# Factors appear in order; restrictions are sorted by arity, so that a
# restriction of arity n is applied after the nth element in every tuple is
# added.

require 'tools'

class NilClass
  def and
    return true
  end
end

module EnumerableOperator

  class Product
    include Enumerable

    attr_reader :factors, :dim

    def initialize(*factors)
      @factors = factors
      @filters = []
      @factors.select {|i| i.is_a?(Proc)}.each {|fn|
        (@filters[(fn.arity.abs) -1] ||= []) << fn} 
        # ( a kludge to get around the fact that {|x|}.arity = -1 )
      @factors.reject!{|i| i.is_a?(Proc)}
      @dim = @factors.length
    end

    def each tuple = [nil]*@dim, i = 0, &block
      @factors[i].each {|x| 
        tuple[i] = x
        args = (i == 0) ? x : tuple[0..i]
        if (@filters[i].and {|f| f.call(*args)})
          if i == @dim - 1 then
            yield tuple.dup 
					else
            each tuple, i + 1, &block 
          end
        end
      }
      self if i==0
    end

    def size
      @factors.product { |enum| enum.size }
    end
  end

  def product(*factors, &block)
    if block
      Product.new(*factors).each(&block)
    else
      Product.new(*factors)
    end
  end
  alias :tuples :product

  module_function :product
end

if __FILE__ == $0

  include EnumerableOperator

  for i in product(1..10, proc {|x| x < 5}, 
                    5..9, proc{|x,y| x+y > 8},
                    21..30, proc{|x,y,z| x * y < z}
                   )
     p i
  end

  for x,y,z in product 1..40, 
                       1..40, proc {|x,y| x <= y},
                       1..40, proc {|x,y,z| x**2 + y**2 == z**2}
    p [x,y,z]
  end

  # restrictions can appear in any order...                     
  product(1..40, 1..40, 1..40, proc{|x,y| x <= y}, proc{|x,y,z| y <= z},
            proc{|x,y,z| x**2 + y**2 == z**2}).each {|x,y,z| 
              puts "#{x}^2 + #{y}^2 = #{z}^2"}
              
  # but note, for instance, that [y <= z] needs a three argument filter, even
  # though x does not appear
end
