'''
October 2019 Puzzler
  10/3/19

You are equipped with two 2's, two 3's, and the ability to combine them using
addition, subtraction, multiplication, division, and exponentiation.

Your job is to create all of the integers from 0 to 36.

You may use any number of parentheses to control the order of operations and 
when possible, all four of the numbers must be used to create a given integer.

For example:  11 = 2^3+3,  however 11 can be created using all four numbers
and therefore, this solution would not be sufficient.    
'''
from __future__ import print_function
from __future__ import division
import itertools as it

class Solution(object):
  '''
  A Solution is a solution to the puzzler.
  '''
  def __init__(self, value, equation):
    self.value = value          #Value is the result of the equation
    self.equation = equation    #The expression that evaluates to value
    self.hash = hash(equation)  #An unique integer for the expression

  def __repr__(self):
    '''
    Text representation of a Solution object.  For example:
      "15 = 3 + (2 + 2) * 3"
    '''
    return str(self.value) + " = " + self.equation

  def __eq__(self, other):
    '''
    Solutions are equal if the equations give identical hashes.
    '''
    return other.hash == self.hash

  def __ne__(self, other):
    return other.hash != self.hash

  def __hash__(self):
    return self.hash

  def __gt__(self, other):
    return self.value > other.value

  def __lt__(self, other):
    return self.value < other.value
##End Solution
  
  
def evaluate(equation):
  '''
    Evaluate equation.  If the result is 0 to 36, create a Solution object
    and add it to the answer set.
  '''
  #Test for and ignore errors
  try:                      
    result = eval(equation)
    resultInt = int(result)
    
    #Test that the result is an integer and is 0 -> 36
    if result == resultInt and result in range(0, 37):
      beforeLength = len(answers)
      a = Solution(resultInt,  equation)
      answers.add(a)
      if len(answers) > beforeLength:   #If answers set grew, then this is a 
        histogram[resultInt] += 1       # another unique solution
                       
  except KeyboardInterrupt:
    quit()

  #We ignore errors, like divide by zero...
  except:
    pass

  
def findInSet(value):
  '''
    Find and return a Solution with the given value as an example of
    an equation that gives the value for display below.
  '''
  for answer in answers:
    if answer.value == value:
      return answer


############    
####Main####  
if __name__ == "__main__":

  answers = set()                       #Create a Set of solutions to remove duplicates
  histogram = [0 for x in range(37)]    #To keep track the number of solutions found
                                        # for each value 0->36

  #All possible arrangements of operands
  operands = it.permutations('2233')

  #All possible arrangements of operators
  # less all exponetiation (results too big) 
  operators =[]                         
  for a in ['+', '-', '*', '/', '**']:
    for b in ['+', '-', '*', '/', '**']:
      for c in ['+', '-', '*', '/', '**']:
        if not (a=="**" and b=="**" and c=="**"):
          operators.append([a, b, c])
   
  #Test each of the operand/operator possibilities
  for d in operands:
    for t in operators:

## Only 4 term equations needed for this one
##
##      #Two term equations
##      eqn = str(d[0]) + " " + str(t[0]) + " " + str(d[1])
##      evaluate(eqn)
##
##      #Three term equations
##      eqn = str(d[0]) + " " + str(t[0]) + " " + str(d[1])  \
##            + " " + str(t[1]) + " " + str(d[2])
##      evaluate(eqn)
##
##      eqn = "(" + str(d[0]) + " " + str(t[0]) + " " + str(d[1]) + ")" \
##            + " " + str(t[1]) + " " + str(d[2])
##      evaluate(eqn)
##      
##      eqn = str(d[0]) + " " + str(t[0]) + " " + "(" + str(d[1]) \
##            + " " + str(t[1]) + " " + str(d[2]) + ")"
##      evaluate(eqn)

      #Four term equations
      eqn = str(d[0]) + " " + str(t[0]) + " " + str(d[1])  \
            + " " + str(t[1]) + " " + str(d[2]) \
            + " " + str(t[2]) + " " + str(d[3])
      evaluate(eqn)

      eqn = "(" + str(d[0]) + " " + str(t[0]) + " " + str(d[1]) \
            + ") " + str(t[1]) + " " + str(d[2]) \
            + " " + str(t[2]) + " " + str(d[3])
      evaluate(eqn)

      eqn = str(d[0]) + " " + str(t[0]) + " (" + str(d[1])  \
            + " " + str(t[1]) + " " + str(d[2]) \
            + ") " + str(t[2]) + " " + str(d[3])
      evaluate(eqn)

      eqn = str(d[0]) + " " + str(t[0]) + " " + str(d[1])  \
            + " " + str(t[1]) + " (" + str(d[2]) \
            + " " + str(t[2]) + " " + str(d[3]) + ")"
      evaluate(eqn)

      eqn = "(" + str(d[0]) + " " + str(t[0]) + " " + str(d[1])  \
            + " " + str(t[1]) + " " + str(d[2]) \
            + ") " + str(t[2]) + " " + str(d[3])
      evaluate(eqn)

      eqn = str(d[0]) + " " + str(t[0]) + " (" + str(d[1])  \
            + " " + str(t[1]) + " " + str(d[2]) \
            + " " + str(t[2]) + " " + str(d[3]) + ")"
      evaluate(eqn)

      eqn = "(" + str(d[0]) + " " + str(t[0]) + " " + str(d[1])  \
            + ") " + str(t[1]) + " (" + str(d[2]) \
            + " " + str(t[2]) + " " + str(d[3]) + ")"
      evaluate(eqn)


##  #Print all solutions found in order by the value
##  answers = sorted(answers)
##  for answer in answers:
##    print(answer)

  #Print the results
  sum = 0
  print("Find solutions using 2, 2, 3, 3 and +, -, *, /, **")
  print("with no or one level of parentheses")
  print()
  print("Num", "\t", "# solns", "\t", "One solution")
  for value in range(len(histogram)):
    eqn = findInSet(value)                      #Find one of the eqns that = value
    print(value, "\t", histogram[value], "\t\t", eqn)
    sum += histogram[value]

  print("Total number of solutions: ", len(answers))