Search code examples
pythonnumpyfunctools

Performing arithmetic calculations on all possible digit combinations in a list


I create data in a format like this:

initial_data = [
"518-2", '533-3', '534-0',
'000-3', '000-4']

I need to perform several operations (add, sub, div, mult, factorial, power_to, root) on the part before the hyphen to see if there's an equation which equals the part after the hyphen.

Like so:

#5182
-5 - 1 + 8 = 2 or 5*(-1) - 1 + 8 = 2

#000-3
number, solution, number_of_solutions
000-3,(0! + 0!) + 0! = 3,2

or
000-4,,0
or 
533-3,5 - (3! / 3) = 3,5

Every digit in the part before the hyphen can have an opposite sign, so I found this:

def inverter(data):

    inverted_data = [-x for x in data]
    res = list(product(*zip(data, inverted_data)))
    return res

I'm supposed to create a CSV file like in the example above but I haven't gotten to that part yet and that seems like the easiest part. What I have are several disparate parts that I can't connect in a sensible way:

import numpy as np
from itertools import product
from math import factorial

def plus(a, b):
    return a + b

def minus(a, b):
    return a - b

def mult(a, b):
    return a * b

def div(a, b):
    if b!=0:
        if a%b==0:
            return a//b
    return np.nan

def the_factorial(a, b):
    try:
        return factorial(int(a))
    except ValueError:
        return np.nan
        
def power_to(a:int, b:int)->int:
    try:
        return int(a**b)
    except ValueError:
        return np.nan

def root(a:int, b:int)->int:
    try:
        return int(b**(1 / a))
    except (TypeError, ZeroDivisionError, ValueError):
        return np.nan

def combinations(nums, funcs):
    """Both arguments are lists"""
    t = []
    for i in range(len(nums)-1):
        t.append(nums)
        t.append(funcs)
    t.append(nums)
    return list(itertools.product(*t))

def solve(instance):
    instance = list(instance)
    for i in range(len(instance)//2):
        b = instance.pop()
        func = instance.pop()
        a = instance.pop()
        instance.append(func(a, b))
    return instance[0]

def main():
    try:
        # a = [1, 3 ,4]
        a = [int(-5), int(-1), int(8)]
        func = [plus, minus, mult, div, the_factorial, power_to, root]
        combs = combinations(a, func)
        solutions = [solve(i) for i in combs]
        for i, j in zip(combs, solutions):
            print(i, j)
    except ValueError:
        #If there's too many combinations
        return np.nan

I'm having trouble transforming the data from the initial_data to inverter to main which currently only works on one example and returns an ugly readout with a function object in the middle.

Thanks in advance.


Solution

  • I think this will help you a lot (tweaks are on you) but it doesn't write in a CSV, I leave that for you to try, just take into account that there are thousands of possible combinations and in some cases, the results are really huge (see comments in main()).

    I've added missing types in function declarations for clarity and successful linting (compatible with older Python versions). Also, I think that the function combinations() is not needed so I removed it. In my proposed code, the function solve() is the one doing the magic :)

    Said all that, here's the full code:

    import numpy as np
    from itertools import product
    from math import factorial
    from typing import Union, Callable, Tuple, List, Set
    
    
    def plus(a: int, b: int) -> int:
        return a + b
    
    
    def minus(a: int, b: int) -> int:
        return a - b
    
    
    def mult(a: int, b: int) -> int:
        return a * b
    
    
    def div(a: int, b: int) -> Union[int, float]:
        try:
            retval = int(a / b)
        except (ValueError, ZeroDivisionError):
            retval = np.nan
        return retval
    
    
    def the_factorial(a: int) -> Union[int, float]:
        try:
            return factorial(int(a))
        except ValueError:
            return np.nan
        except OverflowError:
            return np.inf
    
    
    def power_to(a: int, b: int) -> Union[int, float]:
        try:
            return int(a ** b)
        except (ValueError, ZeroDivisionError):
            return np.nan
    
    
    def root(a: int, b: int) -> Union[int, float]:
        try:
            return int(b ** (1 / a))
        except (TypeError, ZeroDivisionError, ValueError):
            return np.nan
    
    
    def solve(values: Tuple[int, int, int], ops: List[Callable]) -> list[Tuple[str, int]]:
        # Iterate over available functions.
        combs = list()
        for f in FACTORS:
            # Get values to operate with.
            x, y, z = values
            sx, sy, sz = x, y, z
            a, b, c = f
            # Calculate the factorial for the values (if applicable).
            if a == 1:
                sx = f"{x}!"
                x = the_factorial(x)
            if b == 1:
                sy = f"{y}!"
                y = the_factorial(y)
            if c == 1:
                sz = f"{z}!"
                z = the_factorial(z)
            for ext_op in ops:  # External operation.
                for int_op in ops:  # Internal operation.
                    # Create equations by grouping the first 2 elements, e.g.: ((x + y) * z).
                    eq_str = f"{ext_op.__name__}({int_op.__name__}({sx}, {sy}), {sz})"
                    eq_val = ext_op(int_op(x, y), z)
                    combs.append((eq_str, eq_val))
                    # Create equations by grouping the last 2 elements, e.g.: (x + (y * z)).
                    eq_str = f"{ext_op.__name__}({sx}, {int_op.__name__}({sy}, {sz}))"
                    eq_val = ext_op(x, int_op(y, z))
                    combs.append((eq_str, eq_val))
        return combs
    
    
    def inverter(data: List[int]) -> List[Tuple[int, int, int]]:
        inverted_data = [-x for x in data]
        res = list(product(*zip(data, inverted_data)))
        return res
    
    
    # Data to process.
    INITIAL_DATA: List[str] = [
        "518-2",
        '533-3',
        # '534-0',
        # '000-3',
        # '000-4'
    ]
    # Available functions.
    FUNCTIONS: List[Callable] = [   # the_factorial() removed, see solve().
        plus,
        minus,
        mult,
        div,
        power_to,
        root
    ]
    # Get posible combinations to apply the factor operation.
    FACTORS: Set[Tuple] = set(product([1, 0, 0], repeat=3))
    
    
    def main():
        cases = 0       # Count all possible cases (for each input value).
        data = list()   # List with all final data to be dumped in CSV.
        print("number, solution, number_of_solutions")
        # Iterate over all initial data.
        for eq in INITIAL_DATA:
            # Get values before and after the hyphen.
            nums, res = eq.split('-')
            res = int(res)
            # Get combinations with inverted values.
            combs = inverter([int(n) for n in list(nums)])
            # Iterate over combinations and generate a list with their many possible solutions.
            sol_cnt = 0         # Number of solutions (for each input value).
            solutions = list()  # List with all final data to be dumped in CSV.
            for i in [solve(i, FUNCTIONS) for i in combs]:
                for j in i:
                    str_repr, value = j
                    # Some values exceed the 4300 digits, hence the 'try-catch'.
                    # The function 'sys.set_int_max_str_digits()' may be used instead to increase the str() capabilites.
                    try:
                        str(value)
                    except ValueError:
                        value = np.inf
                    if value == res:
                        sol_cnt += 1
                    solutions.append((eq, str_repr, value))
                    cases += 1
            # Iterate over all data gathered, and add number of solutions.
            for i in range(len(solutions)):
                eq, str_repr, value = solutions[i]
                solutions[i] += (sol_cnt,)
                print(f"{eq}, {str_repr} = {value}, {sol_cnt}")
            data.extend(solutions)
            # Print all the solutions for this input.
            print(f"\nThese are the {sol_cnt} solutions for input {eq}:")
            solutions = [s for s in solutions if (type(s[2]) is int and s[2] == res)]
            for i in range(len(solutions)):
                print(f"    {i:4}. {solutions[i][1]}")
            print()
        print(f"\nTotal cases: {cases}")
    

    And for the output, note that solutions are printed/formatted using the name of your functions, not mathematical operators. This is just an excerpt of the output generated for the first value in initial_data using factorials in the 1st and 3rd digits:

    number, solution, number_of_solutions
    518-2, plus(plus(5!, 1), 8!) = 40441, 12
    518-2, plus(5!, plus(1, 8!)) = 40441, 12      
    518-2, plus(minus(5!, 1), 8!) = 40439, 12     
    518-2, plus(5!, minus(1, 8!)) = -40199, 12    
    518-2, plus(mult(5!, 1), 8!) = 40440, 12      
    518-2, plus(5!, mult(1, 8!)) = 40440, 12      
    518-2, plus(div(5!, 1), 8!) = 40440, 12       
    518-2, plus(5!, div(1, 8!)) = 120, 12
    518-2, plus(power_to(5!, 1), 8!) = 40440, 12  
    518-2, plus(5!, power_to(1, 8!)) = 121, 12    
    518-2, plus(root(5!, 1), 8!) = 40321, 12      
    518-2, plus(5!, root(1, 8!)) = 40440, 12
    
    ...
    
    These are the 12 solutions for input 518-2:
           0. plus(minus(-5, 1!), 8)
           1. minus(-5, minus(1!, 8))
           2. plus(minus(-5, 1), 8)
           3. minus(-5, minus(1, 8))
           4. minus(-5, plus(1!, -8))
           5. minus(minus(-5, 1!), -8)
           6. minus(-5, plus(1, -8))
           7. minus(minus(-5, 1), -8)
           8. plus(plus(-5, -1), 8)
           9. plus(-5, plus(-1, 8))
          10. plus(-5, minus(-1, -8))
          11. minus(plus(-5, -1), -8)
    
    Total cases: 4608
    

    Note that 4608 cases were processed just for the first value in initial_data, so I recommend you to try with this one first and then add the rest, as for some cases it could take a lot of processing time.

    Also, I noticed that you are truncating the values in div() and root() so bear it in mind. You will see lots of nan and inf in the full output because there are huge values and conditions like div/0, so it's expected.